/**
 * @preserve VigLink JavaScript Library -- http://www.viglink.com
 *
 * Permission is hereby granted to create derivative works, but only for use
 * with the VigLink web service.
 *
 * Copyright (c) 2012 VigLink. Rights reserved and patent(s) pending.
 */

/**
 * This library uses the VigLink link affiliation service to automatically
 * affiliate external links.  It also provides a simple JavaScript API for
 * affiliating and redirecting to an arbitrary URL.
 *
 * Usage:
 *    1. Set library options as properties of a global vglnk object:
 *      <script type="text/javascript">
 *        var vglnk = { key: "886510a2c5470570ee88f4b7336adf21" };
 *      </script>
 *    2. Include the library: <script src=".../vglnk.js"></script>
 *
 * JavaScript API:
 *    Redirect to a URL, logging the click and rewriting the URL if appropriate.
 *      vglnk.click( "http://www.example.com"[, target] );
 *
 * Public Options:
 *  See initOptions() for defaults; See also: http://viglink.com/corp/api
 *    Required:
 *      key:         {string} Your API key, from http://viglink.com/account.
 *    Optional:
 *      api_url:     {string}  The base url for API calls.
 *      cuid:        {string}  A unique ID to be included in reporting.
 *      dev:         {boolean} Run in dev mode.
 *      enabled:     {boolean} Is the library enabled?
 *      sub_id:      {string}  A "sub-user" ID.  Only useful if you have the
 *                             appropriate agreement in place with VigLink.
 *      reaffiliate: {boolean} Modify already-affiliated links?
 *
 * Private Options:
 *  These options cannot be set by the user.  Documented for reference only.
 *    click_timeout: {number}  Number of milliseconds to wait for a click response.
 *    hop_timeout:   {number}  Number of milliseconds to wait for a hop URL to load.
 *    debug:         {boolean} Log javascript exceptions to the server.
 *    dr_key:        {string}  Legacy Driving Revenue campaign ID
 *    library_id:    {string}  A library instance ID.  Used for reporting, and
 *                             as a signal that the library is intialized.
 *    nofollow:      {Array}   A list of URLs whose links will be marked rel="nofollow".
 *    norewrite:     {Array}   A list of domains whose links will be ignored.
 *    plugins:       {Object}  The default plugin configuration.
 *    swap:          {Array}   A list of URL replacements.
 *    time_click:    {boolean} Time click calls.
 *    time_ping:     {boolean} Time the ping call.
 *
 * Events:
 *  Library events are observed with $.on( viglink, "EventType", fn ).
 *    domready: Fires when it's safe to manipulate the DOM. (Anywhere from
 *              immediately to DOMContentLoaded to window.onload.)
 *    libready: Fires when the library is fully initialized and it's safe to
 *              call API methods.
 *
 * Annotations in this library use Google's derivative of JSDoc.
 * See: http://code.google.com/closure/compiler/docs/js-for-compiler.html
 */

 /*global vglnk_self: true */

( function( self_name ) {
  var viglink;

  /**
 * VigLink library, utility methods
 */

var $; // the collection of library utility methods

/**
 * Note: Method definition is broken in two because certain methods are used
 * during definition of others.
 */
$ = {
  /**
   * Iterate over an array-like object, calling a callback for each item.
   *
   * @param {!Array} list The object to iterate over
   * @param {function(*, number)} fn A callback
   */
  each: function( list, fn ) {
    var i, ii;

    for( i = 0, ii = list.length; i < ii; i++ ) {
      fn( list[i], i );
    }
  },

  /**
   * Extend an Object with properties from one or more other Objects.
   *
   * Objects are processed in order, with properties from later parameters
   * overwriting those from earlier ones.
   *
   * @param {Object} target The target object
   * @param {...Object} The source objects
   * @return {Object} The target object, supplemented with the source objects' properties
   */
  extend: function( target ) {
    // copy reference to target object
    var copy, i, name, options, length = arguments.length;

    target = target || {};

    for( i = 1; i < length; i++ ) {
      // only deal with non-null/undefined values
      options = arguments[i];
      if( options !== undefined && options !== null ) {
        // extend the base object
        for( name in options ) {
          copy = options[name];

          // prevent never-ending loop
          if( target === copy ) {
            continue;
          }

          if( copy !== undefined ) {
            target[name] = copy;
          }
        }
      }
    }

    // Return the modified object
    return target;
  },

  /**
   * Perform no operation. Used as a default value for
   * function references.
   */
  noop: function() {}
};

$.extend( $, {
  /**
   * Add a CSS class name to an element.
   *
   * @param {!Element} el
   * @param {!string} class_name The class name to add
   */
  addClass: function( el, class_name ) {
    if( $.hasClass( el, class_name ) ) {
      return;
    }
    el.className = ( el.className ? el.className + " " : "" ) + class_name;
  },

  /**
   * Get an array of attribute values.
   *
   * @param {!Element} el
   * @param {!string} attr The attribute name
   * @param {!string=} delimiter The token delimter for the attribute values
   */
  attrValues: function( el, attr, delimiter ) {
    delimiter = delimiter || " ";
    attr = el[attr];
    return attr ? attr.split( delimiter ) : [];
  },

  /**
   * Bind a function's to an explicit context.
   *
   * @param {!Function} fn The function to bind
   * @param {!Object} context The new context
   * @return {Function} The passed function, wrapped
   */
  bind: function( fn, context ) {
    return function() {
      return fn.apply( context, arguments );
    };
  },

  /**
   * Basic browser detection. Expand as needed.
   */
  browser: ( function() {
    var ua = {}, version,
        tokens = navigator.userAgent.toLowerCase()
          .replace( /\s*[()]\s*/g, '; ' ).replace( /(\/[\w.]+)\s+/g, '$1; ' )
          .replace( /\;\s*$/, '' ).split( /;\s*/ );

    $.each( tokens, function( token ) {
      version = (/[\/ :]([^\/ :]+)$/.exec( token ) || [])[1];
      ua[version ? token.substr( 0, token.length - version.length - 1 ).replace( /\d*$/, '' ) : token] = version || true;
    } );

    return {
      aol:         ua.aol,
      blackberry:  ua.blackberry,
      ie:          ua.msie,
      ios:         ua.mobile && ua.safari,
      opera:       ua.opera,
      playstation: ua.playstation,
      // Chrome for iOS doesn't set the "version" token. Use "CriOS" token instead.
      version:     parseFloat( ua.version || ua.crios ) || false
    };
  }() ),

  /**
   * Associate arbitrary data with some Object.
   *
   * Warning: The original object is modified! (One new property is added.)
   * This is most useful for associating JS objects with DOM nodes while
   * avoiding memory leaks.
   *
   * @param {!Object} obj The object to which the data will be associated
   * @param {string} name The data's name (the key)
   * @param {?*=} val The data (the value).  If omitted, the cached value is returned.
   * @return {*} The passed or cached value for this object/key
   */
  cache: ( function() {
     // private, persistent variables for this method
     var cache = {},
         expando = "vglnk_" + (new Date()).getTime(),
         uuid = 0;

     return function( obj, name, val ) {
       if( ! obj ) {
         return;
       }

       var id = obj[expando];
       // trying to read from an empty cache?
       if( ! id && val === undefined ) {
         return;
       }
       // compute a unique ID for the element
       if( ! id ) {
         id = ++uuid;
       }
       // new cache?
       if( ! cache[id] ) {
         obj[expando] = id;
         cache[id] = {};
       }
       // saving a value?
       if( val !== undefined ) {
         cache[id][name] = val;
       }
       // return the cached data
       return typeof name === "string" ? cache[id][name] : cache[id];
     };
   }() ),

  /**
   * Get the canonical version of a hostname.
   * e.g. "www.EXAMPLE.com" => "example.com"
   *
   * @param {string|!Location} a The hostname. Either a URL string or an
   *    object which implements the URL decomposition IDL (Location, <a/>, <area/>):
   *    http://www.w3.org/TR/html5-author/urls.html#url-decomposition-idl-attributes
   * @return {string} The canonical hostname
   */
  canonicalizeHostname: function( a ) {
    if( typeof a === "string" ) {
      a = $.createA( a );
    }

    return a.hostname ? a.hostname.toString().toLowerCase()
      .replace( /^www\./, '' )
      .replace( /:.*$/, '' ) // mobile safari's .hostname incorrectly includes the port
      : '';
  },

  /**
   * Clone a simple object.
   *
   * @param {Object} obj
   * @return {Object}
   */
  clone: function( obj ) {
    return $.extend( {}, obj );
  },

  /**
   * Test whether an array contains a value.
   *
   * @param {!Array} list The object to search
   * @param {*} needle The value ot search for
   * @return {boolean}
   */
  contains: function( list, needle ) {
    return $.indexOf( list, needle ) !== -1;
  },

  /**
   * Test whether a string appears to contain personally identifiable information
   *
   * @param {string} str
   * @return {boolean}
   */
  containsPII: function( str ) {
    var email_regex = /\b[A-Z0-9._%+-]+(?:%(?:25)*40|@)[A-Z0-9.-]+\.[A-Z]{2,4}\b/i;
    return email_regex.test( str );
  },

  /**
   * Test whether a browsing context is an ancestor of the current context
   *
   * @param {Window|Frame}
   * @return {boolean}
   */
  contextIsAncestor: function( context ) {
    var current = window.self;

    while( current.parent && current.parent !== current ) {
      current = current.parent;
      if( current === context ) {
        return true;
      }
    }

    return false;
  },

  /**
   * Perform a cross-domain HTTP request, evaluating the response as JavaScript
   *
   * @param {string} url The URL
   */
  cors: function( url ) {
    var request, onSuccess = function() {
      /*jslint evil: true */
      eval( request.responseText );
      /*jslint evil: false */
    };

    request = new window.XMLHttpRequest();
    request.onreadystatechange = function() {
      if( request.readyState === 4 && request.status === 200 ) {
        onSuccess();
      }
    };

    request.open( "GET", url );
    request.withCredentials = true;
    request.send();
  },

  /**
   * Create an <a/> element.
   *
   * @param {string} href A value for the href attribute
   * @param {string=} target A value for the target attribute
   * @return {Element} an <a/> element
   */
  createA: function( href, target ) {
    return $.createEl( "a", { href: href, target: target } );
  },

  /**
   * Create an element.
   *
   * @param {string} name The tag name
   * @param {Object=} attributes An object whose key/value pairs will become tag attributes
   * @param {Object=} css An object whose key/value pairs will become css properties
   * @param {Document=} doc The document in which the element should be created
   * @return {Element}
   */
  createEl: function( name, attributes, css, doc ) {
    var i, el = ( doc || document ).createElement( name );
    attributes = attributes || {};
    css = css || {};

    for( i in attributes ) {
      if( attributes[i] !== undefined ) {
        el[i] = attributes[i];
      }
    }
    $.css( el, css );

    return el;
  },

  /**
   * Set an element's style.
   *
   * @param {Element} el
   * @param {Object=} css An object whose key/value pairs will become css properties
   */
  css: function( el, css ) {
    var i;

    for( i in css ) {
      try {
        el.style[i] = css[i];
      } catch( e ) {}
    }

    return el;
  },

  /**
   * Wrap a function such that it can only be called once.
   * The return value of the first call is cached and returned by subsequent calls.
   *
   * @param {!Function} fn The function to wrap
   * @return {Function} The passed function, wrapped
   */
  destructing: function( fn ) {
    return ( function( fn ) {
      var called = false, return_value;
      return function() {
        if( ! called ) {
          return_value = fn.apply( null, arguments );
          called = true;
        }
        return return_value;
      };
    }( fn ) );
  },

  /**
   * Escape special regular expression characters in a string.
   *
   * @param {string} str
   * @return {string}
   */
  escapeRegExp: ( function() {
    // cache the compiled RegExp
    var regexp;

    return function( str ) {
      regexp = regexp || new RegExp( '([.*+?^${}()|[\\]\\\\])', 'g' );
      return str.replace( regexp, '\\$1' );
    };
  }() ),

  /**
   * Find the link (<a/> or <area/>) element for an event.
   *
   * @param {Event} e
   * @return {?Element} The associated link element, if any
   */
  eventLink: function( e ) {
    var node_type, tag_name, node = e.target || e.srcElement;

    do {
      try {
        node_type = node.nodeType;
      } catch( e ) {
        return;
      }

      if( node_type === 1 ) {
        tag_name = node.tagName.toUpperCase();
        if( tag_name === 'A' || tag_name === 'AREA' ) {
          return node;
        }
      }
      node = node.parentNode;
    } while( node );
  },

  /**
   * Wrap a function such that it conditionally catches and logs exceptions.
   *
   * @param {!Function} fn The function to wrap
   * @param {boolean=} l If true, set the passed method as the logger instead
   *    of wrapping it
   * @return {Function} The passed function, wrapped
   */
  exceptionLogger: ( function() {
    var log = false, logger = $.noop;

    return function( fn, l ) {
      if( l !== undefined ) {
        log = l;
        logger = fn;
      } else {
        return function() {
          if( ! log ) {
            return fn.apply( this, arguments );
          } else {
            try {
              return fn.apply( this, arguments );
            } catch( e ) {
              logger( e );
            }
          }
        };
      }
    };
  }() ),

  /**
   * Deserialize a query string into an object.
   *
   * @param {String} query string
   * @return {Object}
   */
  fromQuery: function( str ) {
    if( str.substr( 0, 1 ) === "?" ) {
      str = str.substr( 1 );
    }

    var pairs = str.split( "&" ), params = {};
    $.each( pairs, function( pair ) {
      var parts = pair.split( "=" );
      params[decodeURIComponent( parts[0] )] = decodeURIComponent( parts[1] );
    } );

    return params;
  },

  /**
   * Find the position and size of the bounding box overlapping one or more elements
   *
   * @param {...Object} The elements
   * @return {Object} An object with properties x, y, w, h
   */
  geometry: function() {
    var i, length = arguments.length,
        x = Infinity, y = Infinity,
        right = -Infinity, bottom = -Infinity,
        position;

    for( i = 0; i < length; i++ ) {
      position = $.position( arguments[i] );

      x = Math.min( x, position.x );
      y = Math.min( y, position.y );
      right = Math.max( right, position.x + arguments[i].offsetWidth );
      bottom = Math.max( bottom, position.y + arguments[i].offsetHeight );
    }

    return {
      x: x,
      y: y,
      w: right - x,
      h: bottom - y,

      x1: x,
      y1: y,
      x2: right,
      y2: bottom
    };
  },

  /**
   * Test whether a specific element attribute contains a certain value
   *
   * @param {Element} el
   * @param {string} attr The attribute name
   * @param {string} value
   * @param {?string} delimiter The value token delimiter
   * @return {boolean}
   */
  hasAttrValue: function( el, attr, value, delimiter ) {
    return attr ? ( $.contains( $.attrValues( el, attr, delimiter ), value ) ) : false;
  },

  /**
   * Test whether an element has a CSS class
   *
   * @param {Element} el
   * @param {string} class_name The CSS class name
   * @return {boolean}
   */
  hasClass: function( el, class_name ) {
    return $.hasAttrValue( el, "className", class_name );
  },

  /**
   * Test whether a link's rel attribute contains a specific relationship
   *
   * @param {Element} a An <a/> element
   * @param {string} rel The relationship value
   * @return {boolean}
   */
  hasRel: function( a, rel ) {
    return $.hasAttrValue( a, "rel", rel );
  },

  /**
   * Find the index of an item in an array.
   *
   * @param {!Array} list The object to iterate over
   * @param {*} needle The value to search for
   * @return {number} The value's index, or -1 if it wasn't found
   */
  indexOf: ( function() {
    // if we've got a native Array.indexOf(), use it.
    if( Array.prototype.indexOf ) {
      return function( list, needle ) {
        return Array.prototype.indexOf.call( list, needle );
      };
    // otherwise, implement our own
    } else {
      return function( list, needle ) {
        var i, ii;

        for( i = 0, ii = list.length; i < ii; i++ ) {
          if( list[i] === needle ) {
            return i;
          }
        }

        return -1;
      };
    }
  }() ),

  /**
   * Test whether an object is an array.
   *
   * @param {Object} a
   * @return {boolean}
   */
  isArray: function( a ) {
    return $.type( a ) === "array";
  },

  /**
   * Test whether an event's default action has been prevented.
   *
   * @param {Event} e
   * @return {boolean}
   */
  isDefaultPrevented: function( e ) {
    return ( e.isDefaultPrevented && e.isDefaultPrevented() ) ||
      e.returnValue === false ||
      e.defaultPrevented === true;
  },

  /**
   * Perform a JSONP call
   *
   * @param {string} url The URL
   */
  jsonp: function( url ) {
    var ref_el = document.getElementsByTagName( "script" )[0],

    script = $.createEl( "script", {
      type: "text/javascript",
      src: url
    } );

    ref_el.parentNode.insertBefore( script, ref_el );
  },

  /**
   * Iterate over an array-like object, calling a callback for each
   * item and building a new array of the results.
   *
   * @param {!Array} list The object to iterate over
   * @param {function(*, number)} fn A callback
   * @return {!Array}
   */
  map: function( list, fn ) {
    var mapped = [];
    $.each( list, function( item, i ) {
      if( item !== undefined ) {
        mapped[i] = fn( item );
      }
    } );
    return mapped;
  },

  /**
   * Register an event handler.
   *
   * @param {Object} obj The object which will fire the event
   * @param {string} type The type of event to listen for
   * @param {Function} fn The event handler
   */
  on: ( function() {
    var default_obj;

    return function( obj, type, fn ) {
      var onevent, wrapped_fn;

      // set the default object
      if( arguments.length === 1 ) {
        default_obj = obj;
        return;

      // listening for one of the default object's events
      } else if( arguments.length === 2 ) {
        if( ! default_obj ) {
          return;
        }

        fn = type;
        type = obj;
        obj = default_obj;
      }

      // chrome will throw a syntax exception on read if the value is invalid
      try { onevent = obj["on" + type]; } catch( e ) {}

      // ensure that return values from direct event handlers are saved (obj.onclick, etc.)
      if( typeof onevent === "function" ) {
        obj["on" + type] = $.bind( function( e ) {
          e = e || window.event;

          // call the original callback outside of the exceptionLogger
          var returnValue = onevent.apply( obj, arguments );

          this.exceptionLogger( function() {
            // Event may not be passed if object.onevent() is called directly by some outside script
            if( e ) {
              // e.returnValue takes precedence if it's been disabled
              if( returnValue !== undefined && e.returnValue !== false ) {
                e.returnValue = returnValue;
              }

              // Firefox 5 has a bug where returning false may not prevent the default action
              // in XHTML 1.0 strict mode.  Ensure it's prevented in this case.
              if( $.isDefaultPrevented( e ) && $.type( e.preventDefault ) === "function" ) {
                e.preventDefault();
              }

              return e.returnValue;
            }

            return returnValue;
          } )();
        }, this );
      }

      // only fire if the library is still enabled
      // TODO: refactor this out of util.js
      wrapped_fn = $.exceptionLogger( function() {
        if( default_obj.enabled() ) {
          return fn.apply( null, arguments );
        }
      } );

      if( obj.addEventListener ) {
        obj.addEventListener( type, wrapped_fn, false );
      } else if( obj.attachEvent ) {
        obj.attachEvent( "on" + type, wrapped_fn );
      // } else {
      //   obj["on" + type] = wrapped_fn;
      }
    };
  }() ),

  /**
   * Find the position of an element on the page.
   *
   * @param {Object} el An element
   * @return {Object|boolean} An object with x and y properties, or false if
   *    the element is not in the DOM.
   */
  position: function( link_el ) {
    var el, xPos = 0, yPos = 0, xScroll = 0, yScroll = 0;

    if( ! link_el.offsetParent ) {
      return false;
    }

    // calculate position
    el = link_el;
    do {
      xPos += el.offsetLeft;
      yPos += el.offsetTop;
      el = el.offsetParent;
    } while( el );

    // calculate scroll offset
    el = link_el;
    do {
      xScroll += el.scrollLeft;
      yScroll += el.scrollTop;
      el = el.parentNode;
    } while( el && el !== document.body );

    return { x: xPos - xScroll, y: yPos - yScroll };
  },

  /**
   * Prevent an event's default action
   *
   * @param {Event} e
   * @return {boolean} false
   */
  preventDefault: function( e ) {
    if( e.preventDefault ) { e.preventDefault(); }
    e.returnValue = false;
    return false;
  },

  /**
   * Call a callback as soon as the document is ready (possibly immdiately).
   *
   * A document is ready when any of these is true:
   *  - document.readyState is "complete"
   *  - DOMContentLoaded has fired (non-IE)
   *  - polling for state indicates DOM is loaded (IE)
   *  - window.onload has fired
   *
   * @param {Function} fn
   */
  ready: ( function() {
    var bound = false, callbacks = [], ready = false,
        bindReady, doScrollCheck, onContentLoaded, onReadyStateChange, onReady;

    // define callbacks for pre-load, ready events
    if( document.addEventListener ) {
      onContentLoaded = function() {
        document.removeEventListener( "DOMContentLoaded", onContentLoaded, false );
        onReady();
      };
    } else if( document.attachEvent ) {
      onReadyStateChange = function() {
        if( document.readyState === "complete" ) {
          document.detachEvent( "onreadystatechange", onReadyStateChange );
          onReady();
        }
      };
    }

    // listen for ready-related events
    bindReady = function() {
      if( bound ) { return; }
      bound = true;

      // browser already ready?
      if( document.readyState === "complete" ) {
        return onReady();

      // browsers that support addEventListener also support DOMContentLoaded
      } else if( document.addEventListener ) {
        document.addEventListener( "DOMContentLoaded", onContentLoaded, false );

      // IE
      } else if( document.attachEvent ) {
        document.attachEvent( "onreadystatechange", onReadyStateChange );

        // if not a frame, poll for DOM readiness
        var top_level = false;
        try {
          top_level = window.frameElement === null;
        } catch(e) {}

        if( document.documentElement.doScroll && top_level ) {
          doScrollCheck();
        }
      }

      // fall back to window.onload
      $.on( window, "load", onReady );
    };

    // poll for DOM readiness in IE
    // credit: Diego Perini -- http://javascript.nwbox.com/IEContentLoaded/
    doScrollCheck = function() {
      if( ready ) { return; }

      try {
        document.documentElement.doScroll( "left" );
      } catch( e ) {
        setTimeout( $.exceptionLogger( doScrollCheck ), 1 );
        return;
      }

      onReady();
    };

    // handle the actual ready event
    onReady = function() {
      if( ready ) { return; }

      // IE and firefox may indicate DOM readiness too early.  Check that body
      // exists. ref: http://github.com/jquery/jquery/commit/262fcf7b#commitcomment-47065
      if( ! document.body ) {
        return setTimeout( $.exceptionLogger( onReady ), 13 );
      }

      ready = true;

      // execute waiting callbacks
      if( callbacks ) {
        $.each( callbacks, function( callback ) {
          callback();
        } );
        callbacks = null;
      }
    };

    // the actual viglink.ready() method
    return function( fn ) {
      bindReady();

      if( ready ) {
        fn();
      } else {
        callbacks.push( fn );
      }
    };
  }() ),

  /**
   * Reformat key names from camelCase to underscore_style.
   *
   * @param {Object} obj
   * @return {Object}
   */
  reformatKeys: function( obj ) {
    var key, new_key, camelCaseToUnderscore = function( s ) { return '_' + s.toLowerCase(); };
    for( key in obj ) {
      new_key = key.replace( /([A-Z])/g, camelCaseToUnderscore );
      if( $.type( obj[key] ) === "object" ) {
        obj[key] = $.reformatKeys( obj[key] );
      }
      if( new_key !== key ) {
        obj[new_key] = obj[key];
        delete obj[key];
      }
    }
    return obj;
  },

  /**
   * Remove a CSS class from an element.
   *
   * @param {Element} el
   * @param {string} class_name
   */
  removeClass: function( el, class_name ) {
    if( $.hasClass( el, class_name ) ) {
      var i, ii, class_names = $.attrValues( el, "className" );

      for( i = 0, ii = class_names.length; i < ii; i++ ) {
        if( class_names[i] === class_name ) {
          delete class_names[i];
        }
      }

      el.className = class_names.join( " " );
    }
  },

  /**
   * Make an HTTP request
   *
   * @param {string} url The base URL
   * @param {Object=} params Query string parameters
   * @param {Object=} call_opts Options (see below)
   * @return {?string} If call_opts.return_url is true, the constructed request URL
   */
  request: function( url, params, call_opts ) {
    var callback, callback_name;

    // default options
    call_opts = $.extend( {
      // @type {Function|string} Callback or callback name
      fn: $.noop,
      // @type {boolean} Should we just return the URL (true), or call immediately (false)
      "return": false,
      // @type {?number} The number of milliseconds to wait for a response
      timeout: null
    }, call_opts );

    // if the callback is just a name, pass it through
    if( typeof call_opts.fn === "string" ) {
      params = $.extend( params, { jsonp: call_opts.fn } );

    // if the callback is a function, make it one-time-use method
    } else if( typeof call_opts.fn === "function" ) {
      // make sure the callback is only called once
      callback = $.destructing( call_opts.fn );
      callback_name = $.uniqid( "vglnk_jsonp_" );
      params = $.extend( params, { jsonp: callback_name } );

      // expose a global callback for direct access by the JSONP response
      window[callback_name] = $.exceptionLogger( function() {
        callback.apply( this, arguments );
        window[callback_name] = undefined;
      } );

      // call the callback if the request times out
      if( call_opts.timeout !== null ) {
        setTimeout( $.exceptionLogger( callback ), call_opts.timeout );
      }
    }

    // for the time being, we're sending every request as a "jsonp" request.
    // in a future version of the server, CORS requests won't need this param.
    params = $.extend( { format: "jsonp" }, params );

    // build the final URL
    params = $.toQuery( params );
    url = url + ( params.length ? '?' : '' ) + params;

    if( call_opts["return"] ) {
       return url;
    } else if( $.traits.cors ) {
      return $.cors( url );
    } else {
      return $.jsonp( url );
    }
  },

  /**
   * Serialize a simple object into a query string.
   *
   * @param {Object} obj
   * @return {string}
   */
  toQuery: function( obj ) {
    var k, qs = '';
    for( k in obj ) {
      if( obj[k] !== null && obj[k] !== undefined ) {
        qs += '&' + encodeURIComponent( k ) + '=' + encodeURIComponent( obj[k] );
      }
    }
    return qs.substr( 1 );
  },

  /**
   * Test whether a request to a URL will appear to transmit personally identifiable information.
   *
   * @param {string} request
   * @return {boolean}
   */
  transmitsPII: function( request ) {
    return $.containsPII( request + " " + document.referrer );
  },

  /**
   * A more robust alternative to the typeof operator.
   *
   * @param {Object} obj
   * @return {string}
   */
  type: function( obj ) {
    return obj === null ?
        "null" :
      obj === undefined ?
        "undefined" :
      Object.prototype.toString.call( obj ).slice( 8, -1 ).toLowerCase();
  },

  /**
   * Update specific parts of a URL
   *
   * @param {string} url The base URL
   * @parts {Object} parts The parts of the URL to update.
   *    Valid properties are any of the URL decomposition IDL attributes:
   *    http://www.w3.org/TR/html5-author/urls.html#url-decomposition-idl-attributes
   * @return {string} The updated URL
   */
   updateUrl: function( url, parts ) {
     return $.extend( $.createA( url ), parts ).href;
   },

  /**
   * Generate a pseudo-unique ID.
   *
   * @param {string=} prefix
   * @return {string}
   */
  uniqid: ( function() {
    var counter = 0;

    return function( prefix ) {
      return ( prefix || "" ) + (new Date()).getTime() + counter++;
    };
  }() )
} );

/**
 * Browser traits, used to determine library behavior
 */
$.traits = {
  // base level library compatibility
  basicCompatibility: ! ( $.browser.blackberry || $.browser.playstation ),
  // cross-domain ajax
  cors: window.XMLHttpRequest && ( new window.XMLHttpRequest() ).withCredentials !== undefined,
  // browser properly handled cross-window, same-origin communication
  crossWindowCommunication: ! ( $.browser.ios && $.browser.version && ( $.browser.version < 5 || $.browser.version > 5 ) ),
  // does a js-based redirect set the referrer?
  jsRedirectSetsReferrer: $.browser.aol || ! ( $.browser.ie || $.browser.opera ),
  // is the browser in strict mode? (for browsers where that matters)
  quirksMode: ! Boolean( window.addEventListener ),
  // can we listen for clicks at the window level?
  windowLevelHandlers: Boolean( window.addEventListener )
};
  /**
 * VigLink library, core methods
 */

/*global $: false, self_name: true */

var opts,    // library options. see initOptions() for defaults, or the docs in vglnk.js
    scope,   // a reference to the public vglnk object's parent (which may not be window)
    self,    // a reference to the public vglnk object (which may not be window.vglnk)
    viglink; // the private namespace for core library methods

/**
 * Core methods
 */
viglink = {
  /**
   * Perform a REST API call.
   */
  api: function( method, params, call_opts ) {
    var url = opts.api_url + "/" + method;

    params = $.extend( {
      cuid: opts.cuid,
      key: opts.key,
      drKey: opts.key ? null : opts.dr_key,
      loc: location.href,
      subId: opts.sub_id,
      v: 1
    }, params );

    // if a plugin overrode the api key, we shouldn't send subId (since it's
    // the subId of the original api key's user, not the new one)
    if( params.subId && params.key !== opts.key ) {
      params.subId = null;
    }

    return $.request( url, params, call_opts );
  },

  /**
   * Register a listener for a custom library event.
   *
   * The recommended way to register a library event listener is not to call
   * this method directly, but to instead use $.on( viglink, ... );
   */
  addEventListener: function( type, fn ) {
    this.fire( type, fn );
  },

  /**
   * Asynchronously redirect to a URL, rewriting it along the way.
   *
   * Usage: click( <A/> ), click( "http://..."[, target] )
   */
  click: ( function() {
    var

    // determine which API request type to use
    getApiFormat = function( context, target ) {
      // use a server-side redirect for all targeted links in browsers that
      // don't set referrer after a javascript redirect
      if( target !== "_self" && ! ( $.traits.crossWindowCommunication && $.traits.jsRedirectSetsReferrer ) ) {
        return "go";
      }

      // test cross-document access, falling back to redirection if access to
      // to the target document is denied
      try {
        if( context.document === undefined ) { throw true; }
        return "jsonp";
      } catch( e ) {
        return "go";
      }
    },

    // find/create the browsing context for this link
    getBrowsingContext = function( target ) {
      if( target === "_self" ) {
        return window.self;
      } else if( $.traits.crossWindowCommunication && $.traits.jsRedirectSetsReferrer ) {
        var context = window.open( '', target );
        context.focus();
        return context;
      }
    },

    // log a link's text context
    logContext = function( link ) {
      var params,
          bNode = link.previousSibling, aNode = link.nextSibling,
          parts = ['', link.textContent, ''],
          word_limit = 10,

      normalizeSpace = function( a, b, c ) {
        var normalize = function( v ) {
          return v.replace( /\s+/g, ' ' );
        };

        // normalize whitespace as a browser would for display
        a = normalize( a );
        b = normalize( b );
        c = normalize( c );

        a = a.replace( /^\s+/, '' );
        if( b.substr( 0, 1 ) === ' ' ) {
          b = b.substr( 1 );
          a += a.substr( a.length - 1, 1 ) !== ' ' ? ' ' : '';
        }
        if( b.substr( b.length - 1, 1 ) === ' ' ) {
          b = b.substr( 0, b.length - 1 );
          c = ( c.substr( 0, 1 ) !== ' ' ? ' ' : '' ) + c;
        }
        c = c.replace( /\s+$/, '' );

        return [a, b, c];
      };

      // for now we're only accepting the absolute simplest case:
      //   [text node]<link/>[text node]
      if( parts[1] !== undefined ) {
        parts[0] = bNode && bNode.nodeType === 3 ? bNode.data : '';
        parts[2] = aNode && aNode.nodeType === 3 ? aNode.data : '';

        parts = normalizeSpace.apply( this, parts );

        if( parts[0] !== '' && parts[2] !== '' ) {
          // apply the word limit
          parts[0] = parts[0].split( ' ' ).reverse().slice( 0, word_limit + ( parts[0].substr( parts[0].length - 1, 1 ) === ' ' ? 1 : 0 ) ).reverse().join( ' ' );
          parts[2] = parts[2].split( ' ' ).slice( 0, word_limit ).join( ' ' );

          // log it
          params = {
            type: "context",
            itype: ( $.cache( link, "params" ) || {} ).type,
            before: parts[0],
            after: parts[2],
            txt: parts[1],
            loc: location.href,
            out: link.href,
            v: 2
          };

          viglink.log( "info", $.toQuery( params ) );
        }
      }
    };

    // the actual viglink.click() method
    return function( link, target ) {
      // normalize the target so that links to this window always target "_self"
      target = link.target || target;
      target = (
        ! target ||
          target === window.name ||
        ( target === "_top"    && window.top === window.self ) ||
        ( target === "_parent" && window.parent === window.self )
      ) ? "_self" : target;

      var callback, params,
          context = getBrowsingContext( target ),
          format = getApiFormat( context, target );

      // generate a callback method for the click request
      callback = $.destructing( $.bind( function() {
        if( opts.time_click && arguments.length ) {
          this.logTime( "clk" );
        }
        var args = Array.prototype.slice.call( arguments );
        args.unshift( link, context, target );
        this.onApiClick.apply( this, args );
      }, this ) );

      // save the last link href so we can log it if there's an exception
      $.cache( this, "link", typeof link === "string" ? link : link.href );

      // if we were passed a URL string, convert it to an <a/> element
      if( typeof link === "string" ) {
        link = $.createA( link, target );

        // perform checks that are normally done up-front for existing links
        if( ! this.preprocessLink( link ) ) {
          // short-circuit the API call if it shouldn't be rewritten
          return callback();
        }
      }

      // if we're disabled but still got called (via vglnk.click()), do the
      // redirect without a rewrite
      if( ! opts.enabled ) {
        return callback();
      }

      params = this.clickParams( link, format );

      // save the current time for request timing
      this.logTime();

      // save the link's text context for link insertion tuning
      if( opts.log_context ) {
        logContext( link );
      }

      // redirect
      if( format === "go" ) {
        this.redirect( this.api( "click", params, { "return": true } ), context, target );

      // loading in this window w/ normal (background) JSONP
      } else if( context === window.self ) {
        // call the click API
        this.api( "click", params, {
          fn: callback,
          timeout: opts.click_timeout
        } );

      // if the target browsing context is an ancestor of the current one, load unaffiliated
      } else if( $.contextIsAncestor( context ) ) {
        return this.redirect( link.href, context, target );

      // load in another browsing context w/ the JSONP call as a direct asset
      } else {
        callback = $.exceptionLogger( callback );

        // guard against click timeouts
        setTimeout( function() { callback(); }, opts.click_timeout );

        // write a temporary document which loads the JSONP request directly
        context.document.open();
        context.callback = callback;
        context.document.write(
          '<html><head>' +
            '<title>' + link.href + '</title>' +
            '<script type="text/javascript" src="' +
              this.api( "click", params, { fn: "callback", "return": true } ) +
            '"></script>' +
          '</head></html>'
        );
        context.document.close();
      }
    };
  }() ),

  /**
   * Build the set of parameters for a click API call.
   */
  clickParams: function( link, format ) {
    var params = $.extend( $.cache( link, "params" ), {
      // standard click options
      format: format,
      libid: opts.library_id,
      out: link.href,
      ref: document.referrer || null,
      reaf: opts.reaffiliate || null,
      title: document.title,
      txt: link.innerHTML
    } );

    // IE has trouble with very long query strings (>2k).
    if( params.txt.length > 128 ) {
      // try to strip HTML
      params.txt = params.txt.replace( /<[^>]+>/g, '' );
      // still too long? truncate it.
      params.txt = params.txt.length > 128 ? params.txt.substr( 0, 125 ) + "..." : params.txt;
    }

    return params;
  },

  /**
   * Disable the library if it's a an indirect installation and a direct
   * installation exists.
   */
  enabled: function() {
    if( opts.enabled &&
        // indirection installation
        scope !== window &&
        // direct installation exists
        window.vglnk && ( window.vglnk.key || typeof window.vglnk === "function" ) ) {
      opts.enabled = false;
    }
    return opts.enabled;
  },

  /**
   * Expose an internal method as part of the public JavaScript API.
   */
  expose: function( name, target ) {
    target = target || this[name];

    if( target ) {
      if( typeof target === "function" ) {
        target = $.exceptionLogger( $.bind( target, this ) );
      } else if( typeof target === "object" ) {
        target = $.clone( target );
      }

      // for now we won't allow API methods to be overwritten
      if( ! self[name] ) {
        self[name] = target;
      }
    }
  },

  /**
   * Fire an event, calling registered listeners.
   */
  fire: ( function() {
    var events = {};

    return function( type, listener ) {
      type = type.toLowerCase();

      var e = events[type] || { fired: false, listeners: [] },
        callListener = function( listener ) { listener( { type: type } ); };

      // register a listener
      if( typeof listener === "function" ) {
        if( e.fired ) {
          callListener( listener );
        } else {
          e.listeners.push( listener );
        }

      // call listeners
      } else {
        e.fired = true;
        $.each( e.listeners, function( listener ) {
          if( typeof listener === "function" ) {
            callListener( listener );
          }
        } );
        e.listeners = [];
      }

      events[type] = e;
    };
  }() ),

  /**
   * Handle the various phases of right-clicking on a link.
   */
  handleRightClick: function( link, phase ) {
    if( ! ( opts.rewrite_modified && link && phase ) ) {
      return;
    }

    switch( phase ) {
      case "setup":
        // save the original href
        if( ! $.cache( link, "href" ) ) {
          $.cache( link, "href", link.href );
        }

        // swap in our API href
        link.href = this.api( "click", this.clickParams( link, "go" ), { "return": true } );

        // reset the link href ASAP
        setTimeout( $.exceptionLogger( $.bind( function() {
          this.handleRightClick( link, "teardown" );
        }, this ) ), 0 );
        break;

      case "teardown":
        link.href = $.cache( link, "href" );
        $.cache( link, "href", null );
        break;
    }
  },

  /**
   * Check that some operation is allowed by the current harmony score.
   *
   * The harmony score is a very simple "passive/aggressive" compatibility setting.
   */
  harmony: ( function() {
    $.harmony = {
      UNSAFE_QUIRKSMODE_HANDLERS: 0,
      LINK_LEVEL_EVENTS: -1
    };

    return function( value ) {
      return opts.harmony_level <= value;
    };
  }() ),

  /**
   * Initialize the library, setting options.
   */
  init: $.exceptionLogger( function() {
    var shouldInitialize = function() {
      var prop = "__v5k",

      // has another instance already been initialized?
      already_initialized = document[prop] === true;

      document[prop] = true;
      return ! already_initialized;
    };

    return function() {
      if( shouldInitialize() ) {
        this.initLibEvents();
        this.initNamespace();
        this.initOptions();

        // set the logger method and condition
        $.exceptionLogger( $.bind( this.logException, this ), ! opts.dev );

        this.initDRApi();
        this.initApi();

        if( this.enabled() ) {
          this.initLegacyCallbacks();
          this.ping();
        }
      }
    };
  }() ),

  /**
   * Initialize the JavaScript API
   */
  initApi: function() {
    // preserve library plugin definitions as children of window.vglnk, so
    // that they're loaded by other library instances
    var i, plugins = {};

    // temporarily move plugins out of the way
    if( window.vglnk ) {
      for( i in window.vglnk ) {
        if( i.substr( -7 ) === "_plugin" ) {
          plugins[i] = window.vglnk[i];
        }
      }
    }

    // define a function so that old-style vglnk() calls don't error
    self = scope[self_name] = $.noop;

    // expose some internal methods as a JS API
    this.expose( "click" );
    this.expose( "link", viglink.preprocessLink );
    this.expose( "opt" );
    this.expose( "$", $.clone( $ ) );

    // restore plugin definitions to their public place
    $.extend( self, self === window.vglnk ? plugins : {} );
  },

  /**
   * Support legacy Driving Revenue APIs
   */
  initDRApi: function() {
    var called = false;

    window.DrivingRevenue = $.exceptionLogger( $.destructing( $.bind( function() {
      called = true;
      opts.dr_key = window.DR_id;
      if( this.enabled() ) {
        this.ping();
      }
    }, this ) ) );

    // clean up the global DrivingRevenue() if it wasn't needed
    $.on( "DOMReady", function() {
      if( ! called ) {
        try {
          delete window.DrivingRevenue;
        } catch( e ) {
          window.DrivingRevenue = undefined;
        }
      }
    } );
  },

  /**
   * Set up custom events
   */
  initLibEvents: function() {
    // register the viglink object as the default event object
    $.on( viglink );

    // fire the custom DOMReady event as soon as possible
    $.ready( $.bind( function() { this.fire( "DOMReady" ); }, this ) );
  },

  /**
   * Initialize the library's namespace.
   */
  initNamespace: function() {
    // if there's a direct installation waiting to be initialized, load it instead
    if( window.vglnk && window.vglnk.key ) {
      self_name = "vglnk";
    }

    var current = window,
        parts = self_name.split( "." ),
        property;

    self_name = parts.pop();

    while( parts.length > 0 ) {
      property = parts.shift();
      current[property] = ( current[property] || {} );
      current = current[property];
    }

    scope = current;
    self = scope[self_name] = ( scope[self_name] || {} );
  },

  /**
   * Aggregate options from defaults and user settings.
   */
  initOptions: function() {
    var i;

    this.initLegacyOptions();

    opts = $.extend( this.publicOptions( {
      // public options (may be set by the user prior to including this library)
      api_url:     '//api.viglink.com/api',
      cuid:        null,
      dev:         false,
      dr_key:      null,
      enabled:     $.traits.basicCompatibility,
      key:         null,
      partner:     null,
      sub_id:      null,
      reaffiliate: false,
      // public so they can be set by plugins
      harmony_level:    0,     // set by the harmony_score plugin
      rewrite_original: true,  // set by onApiPing() and the link_affiliation plugin
      rewrite_modified: false  // set by the modified_clicks plugin
    } ), opts, self, {
      // private options (may not be set by the user.  most are set by the ping response)
      click_timeout: 1000,
      hop_timeout:   2000,
      debug:         false,
      library_id:    null,
      log_context:   true,
      nofollow:      {},
      norewrite:     {},
      plugins: { // plugins which are enabled by default
        link_affiliation: {},
        modified_clicks:  {}
      },
      swap:          {},
      time_click:    false,
      time_ping:     false
    } );

    // if a plugin has been loaded among the options, remove it
    for( i in opts ) {
      if( i.substr( -7 ) === "_plugin" ) {
        delete opts[i];
      }
    }
  },

  /**
   * Support legacy JS APIs by mapping old options to their new counterparts.
   */
  initLegacyOptions: function() {
    var i, settings_map = {
      DR_id:         "dr_key",
      vglnk_api_key: "key",
      vglnk_cuid:    "cuid",
      vglnk_domain:  "api_url",
      vglnk_reaf:    "reaffiliate",
      vglnk_subid:   "sub_id"
    };

    // read vglnk_* options
    for( i in settings_map ) {
      if( window[i] !== undefined ) {
        self[settings_map[i]] = window[i];
        if( i === "vglnk_domain" ) {
           self[settings_map[i]] += "/api";
        }
      }
    }
  },

  /**
   * Support legacy JS APIs by exposing old JSON-P callbacks.
   */
  initLegacyCallbacks: function() {
    var i, callback_map = {
      vl_cB:      $.bind( this.onApiClick, this ),
      vl_disable: function(){ opts.enabled = false; }
    };

    // expose callbacks globally
    for( i in callback_map ) {
      window[i] = callback_map[i];
    }
  },

  /**
   * Iniitalize all links in the document.
   */
  initLinks: function() {
    $.each( document.links, $.bind( this.preprocessLink, this ) );
  },

  /**
   * Initialize plugins
   * Set plugin options from the server.  Begin polling for plugins.
   */
  initPlugins: ( function(){
    var timeout,
        min_interval = 100, max_interval = 5000, iteration = 1,
        waiting_plugins = [],

    /**
     * Poll for new plugins to load.  Only plugins enabled via the ping
     * response are loaded.
     */
    pollForPlugins = function() {
      var still_waiting_plugins = [], makeLogger = function( plugin_key ) {
        return function() {
          if( plugin_key ) {
            var args = Array.prototype.slice.call( arguments );
            args.unshift( "custom", plugin_key );
            viglink.log.apply( this, args );
          }
        };
      };

      timeout = null;

      $.each( waiting_plugins, function( i ) {
        var run, setup,
            name = i[0], opts = i[1], ran_setup = i[2],
            definition = window.vglnk && window.vglnk[name + "_plugin"];

        // enabled, but not yet defined. keep waiting.
        if( ! definition ) {
          still_waiting_plugins.push( i );

        // plugin is defined and enabled. load it.
        } else {
          run = $.type( definition.run ) === "function" ? definition.run : definition;
          setup = $.type( definition.setup ) === "function" ? definition.setup : null;

          if( opts && opts.mode === 1 ) {
            if( setup ) {
              setup( opts, $.clone( $ ), self, makeLogger( opts.key ) );
            }
            delete opts.mode;
          } else if( run ) {
            if( setup && ! ran_setup ) {
              setup( opts, $.clone( $ ), self, makeLogger( opts.key ) );
            }
            run( opts, $.clone( $ ), self, makeLogger( opts.key ) );
          }
        }
      } );

      waiting_plugins = still_waiting_plugins;

      // still waiting for an enabled plugin?
      if( waiting_plugins.length > 0 ) {
        timeout = setTimeout( $.exceptionLogger( pollForPlugins ), Math.min( Math.max( Math.pow( 2, ++iteration ), min_interval ), max_interval ) );
      }
    },

    generateInitFunction = function( pname, popts ) {
      return ( function( name, opts ) {
        return function() {
          waiting_plugins.push( [ name, opts, true ] );

          // reset the polling backoff and try to load it immediately
          iteration = 1;
          clearTimeout( timeout );
          pollForPlugins();
        };
      }( pname, popts ) );
    };

    return function( plugin_opts ) {
      var i;

      for( i in plugin_opts ) {
        if( typeof plugin_opts[i] === "object" && plugin_opts[i].enabled !== false ) {
          waiting_plugins.push( [ i, plugin_opts[i] ] );

          if( plugin_opts[i].mode === 1 ) {
            this.expose( "init_" + i, generateInitFunction( i, plugin_opts[i] ) );
          }
        }
      }

      // begin polling for plugins
      pollForPlugins();
    };
  }() ),

  /**
   * Test whether a link should be rewritten.
   */
  isRewritable: ( function() {
    // private, persistent variables for this method
    var document_host = $.canonicalizeHostname( document.location );

    return function( link ) {
      var link_host, link_scheme = '';

      // skip links which we do not have permission to access. (IE throws
      // a security exception if the href is "user:password@host", for example)
      try {
        link_host = link.hostname;
        link_scheme = link.protocol;
        // call any method to force Closure Compiler to preserve this block.
        // (workaround for http://code.google.com/p/closure-compiler/issues/detail?id=223)
        link_host.charAt( 0 );
      } catch( e ) {
        return false;
      }

      if( link_host !== "" ) {
        link_host = $.canonicalizeHostname( link );
      }

      return ! (
        link_host === ""                    ||  // just an <a/>, not a link (no href)
        document_host === link_host         ||  // internal link
        ! link_scheme.match( /^https?:$/i ) ||  // non-http(s) link
        opts.norewrite[link_host]           ||  // server-marked norewrite (a/k/a "skip domain")
        ! ( opts.rewrite_original || $.cache( link, "type" ) ) || // skip publisher-authored links if affiliation is disabled
        $.hasRel( link, "norewrite" ) || $.hasRel( link, "noskim" ) // rel="norewrite"
      );
    };
  }() ),

  /**
   * Initialize event handlers.
   */
  initEvents: $.destructing( function() {
    var

    click_listeners = {
      left: viglink.onClick,
      right: viglink.onContextmenu
    },

    noop_listeners = {
      left: $.noop,
      right: $.noop
    },

    target = $.traits.windowLevelHandlers ? window : document,

    addLinkListeners = function( e ) {
      e = e || window.event;
      var link = $.eventLink( e ), handlers = {};

      if( link && ! $.cache( link, "evented" ) ) {
        registerClickHandlers( link, viglink.harmony( $.harmony.LINK_LEVEL_EVENTS ) ? click_listeners : noop_listeners );
        $.cache( link, "evented", true );
      }
    },

    registerClickHandlers = function( target, listeners ) {
      $.on( target, "click",       $.bind( listeners.left, viglink ) );
      $.on( target, "contextmenu", $.bind( listeners.right, viglink ) );
    };

    // log selections when copied for phrase hinting
    $.on( target, "copy", $.bind( viglink.onCopy, viglink ) );
    $.on( target, "mousedown", addLinkListeners );

    // use more aggressive link-level event handling
    if( viglink.harmony( $.harmony.LINK_LEVEL_EVENTS ) ) {
      $.on( "DOMReady", function() {
        $.each( document.links, function( link ) {
          $.on( link, "mousedown", addLinkListeners );
        } );
      } );
    }

    if ( ! $.traits.quirksMode || viglink.harmony( $.harmony.UNSAFE_QUIRKSMODE_HANDLERS ) ) {
      registerClickHandlers( target, click_listeners );
    }
  } ),

  /**
   * Log a message to the API server.
   */
  log: function( type, msg, details, custom_params ) {
    var params, plugin_key, url, query_string = $.toQuery( { nocache: $.uniqid() } ), target = "pixel.gif";

    if( type === "custom" ) {
      plugin_key = msg;
      type = details;
      query_string += "&" + $.toQuery( { key: plugin_key, type: type } );
      $.each( $.type( custom_params ) === "array" ? custom_params : [custom_params], function( params ) {
        // remove params used by other log calls (which may result in this call being miscategorized)
        $.each( ['e', 'i', 'o'], function( n ) { delete params[n]; } );
        query_string += "&" + $.toQuery( params );
      } );
    } else {
      query_string += "&" + $.toQuery( { key: opts.key, drKey: opts.key ? null : opts.dr_key } );
      if( type === "time" ) {
        target = "time.gif";
        params = {
          libId: opts.library_id,
          time: details,
          type: msg
        };
      } else if( type === "exception" ) {
        params = { e: msg, o: details };
      } else if( type === "info" ) {
        params = { i: msg };
      } else {
        return; // unknown type
      }

      query_string += "&" + $.toQuery( params );
    }

    url = opts.api_url + '/' + target + '?' + query_string;
    if( ! $.transmitsPII( url ) ) {
      $.createEl( "img" ).src = url;
    }
  },

  /**
   * Log an Exception to the server.
   */
  logException: function( ex ) {
    if( opts.debug ) {
      var details = {
        link: $.cache( this, "link" ),
        loc: document.location.href,
        UA: navigator.userAgent
      };

      if( typeof ex === "string" ) {
        details.message = ex;
      } else {
        details = $.extend( details, ex );
      }

      this.log( "exception", ex, $.toQuery( details ) );
    }
  },

  /**
   * Log timing info for some event to the server.
   */
  logTime: ( function() {
    var start_time;

    return function( type ) {
      if( arguments.length === 0 ) {
        start_time = ( new Date() ).getTime();
      } else {
        this.log( "time", type, ( new Date() ).getTime() - start_time );
      }
    };
  }() ),

  /**
   * Handle the /api/click JSONP response.
   */
  onApiClick: function( link, context, target, result_url, click_opts ) {
    var i,
        url = result_url || link.href,
        redirect = $.bind( function() { this.redirect( url, context, target ); }, this );

    // is there a hop URL for this link?
    if( typeof click_opts === "object" && ( click_opts.tracking || click_opts.image ) ) {
      // load the hop URL with either an iframe or image
      i = $.createEl( click_opts.tracking ? "iframe" : "img",
        { src: click_opts.tracking || click_opts.image },
        { height: 0, width: 0, visibility: "hidden" }
      );
      document.body.appendChild( i );
      // give it some time to load and redirect to the real URL
      setTimeout( $.exceptionLogger( redirect ), click_opts.timeout || opts.hop_timeout );
    } else {
      redirect();
    }
  },

  /**
   * Handle the /api/ping JSONP response.
   */
  onApiPing: function( library_id, click_timeout, norewrite, swap, ping_opts, nofollow ) {
    opts.rewrite_original = false;
    ping_opts = $.reformatKeys( ping_opts || {} );

    var i, toHash, plugin_opts;

    toHash = function( arr ) {
      var hash = {},

      iter = function( arg ) {
        if( $.isArray( arg ) ) {
          hash[arg[0]] = arg[1];
        } else {
          hash[arg] = 1;
        }
      };

      if( $.isArray( arr ) ) {
        $.each( arr, iter );
      }

      return hash;
    };

    // propagate library and plugin options
    plugin_opts = $.extend( opts.plugins, ping_opts.plugins );
    opts = $.extend( opts, ping_opts );
    delete opts.plugins;

    opts.library_id = library_id;
    opts.click_timeout = click_timeout;

    if( opts.time_ping ) {
      this.logTime( "png" );
    }

    // convert arrays of values into objects for fast lookup
    $.extend( opts.norewrite, toHash( norewrite ) );
    $.extend( opts.swap,      toHash( swap ) );
    $.extend( opts.nofollow,  toHash( nofollow ) );

    // register event handlers passed as options
    for( i in opts ) {
      if( i.toLowerCase().substr( 0, 2 ) === "on" && i.length > 2 && $.type( opts[i] ) === "function" ) {
        $.on( viglink, i.toLowerCase().substr( 2 ), $.bind( opts[i], window ) );
        delete opts[i];
      }
    }

    this.initPlugins( plugin_opts );
    this.initLinks();
    this.initEvents();

    this.fire( "libready" );
  },

  /**
   * Handle a link's click event.
   */
  onClick: function( e ) {
    e = e || window.event;

    var keyboard_modified = e.ctrlKey || e.metaKey || e.altKey || e.shiftKey,
        left_click = ( e.which && e.which === 1 ) || e.button === 0,
        link = $.eventLink( e );

    if( ! link || ! this.isRewritable( link ) || keyboard_modified || ! left_click || $.isDefaultPrevented( e ) ) {
      return;
    }

    this.click( link );

    return $.preventDefault( e );
  },

  /**
   * Handle a link's contextmenu event.
   */
  onContextmenu: function( e ) {
    var link = $.eventLink( e || window.event );

    if( link && this.isRewritable( link ) ) {
      this.handleRightClick( link, "setup" );
    }
  },

  /**
   * Handle a link's click event.
   */
  onCopy: function( e ) {
    var i, ii, selection, str, text = [], MAX_LEN = 128;

    if( window.getSelection ) {
      selection = window.getSelection();
      for( i = 0, ii = selection.rangeCount; i < ii; i++ ) {
        try {
          str = selection.getRangeAt( i ).toString().replace( /((^)\s+|\s+$|\r)/g, "" ).replace( /\s*\n\s*/g, "\n" );
        } catch( e ) {}

        if( str.length > 0 && str.length <= MAX_LEN ) {
          text.push( str );
        }
      }
    }

    $.each( text, function( str ) {
      viglink.log( "info", $.toQuery( {
        type: "selection",
        txt: str,
        loc: location.href
      } ) );
    } );
  },

  /**
   * An external interface for getting an option's value.  Intended for use
   * in library plugins.
   */
  opt: function( name, value ) {
    if( value !== undefined && this.publicOptions()[name] !== undefined ) {
      opts[name] = value;
    }
    return opts[name];
  },

  /**
   * Initiate an API ping call.
   */
  ping: ( function() {
    var called_ping = false;

    return function() {
      if( ! called_ping && ( opts.key || opts.dr_key ) ) {
        called_ping = true;

        if( $.transmitsPII( this.api( "ping", null, { "return": true } ) ) ) {
          return;
        }

        // save the current time for request timing
        this.logTime();
        this.api( "ping", null, { fn: $.bind( this.onApiPing, this ) } );
      }
    };
  }() ),

  /**
   * Make any updates to a link that don't require a round-trip.
   */
  preprocessLink: function( link ) {
    if( ! this.isRewritable( link ) ) {
      return;
    }

    // unravel a click api link?
    var link_params, api_url = $.createA( opts.api_url );
    if( link.pathname === "/api/click" &&
        ( link.hostname === api_url.hostname ||
          link.hostname.match( /(^|\.)(api|cdn|apicdn)\.viglink\.com$/ ) )
    ) {
      link_params = $.fromQuery( link.search );
      if( link_params.out !== undefined ) {
        link.href = link_params.out;
        delete link_params.out;
        $.cache( link, "params", link_params );
      }
    }

    // swap it?
    if( opts.swap[link.href] ) {
      link.href = opts.swap[link.href];
    }

    // add nofollow?
    if( opts.nofollow[link.href] && ! $.hasRel( link, "nofollow" ) ) {
      link.rel = ( link.rel ? link.rel + ' ' : '' ) + "nofollow";
    }

    // avoid an IP.Board bug by converting their rel="external" links to target="_blank"
    if( window.IPBoard && window.IPBoard.prototype &&
        window.IPBoard.prototype.delegate && $.hasRel( link, "external" ) ) {
      link.rel = link.rel.replace( /(^| )external( |$)/, "" );
      link.target = "_blank";
    }

    return link;
  },

  /**
   * Set/Get the set of public options.
   */
  publicOptions: ( function() {
    var public_opts = {};
    return function( popts ) {
      if( $.type( popts ) === "object" ) {
        public_opts = popts;
      }
      return $.extend( {}, public_opts );
    };
  }() ),

  /**
   * Redirect to a new URL.
   *
   * @param {String} url The URL to load
   */
  redirect: function( url, context, target ) {
    var a, w;

    // window may not have been opened yet (if ! $.traits.crossWindowCommunication)
    if( ! $.traits.crossWindowCommunication && ! context ) {
      w = window.open( url, target );
      w.focus();

    // IE only passes referrer if the link is "clicked"
    } else if( ! $.traits.jsRedirectSetsReferrer ) {
      if( target === "_blank" ) {
        target = $.uniqid( "win_" );
      }

      a = $.createA( url, target );
      a.rel = "norewrite";
      document.body.appendChild( a );
      a.click();
      a.parentNode.removeChild( a );

    } else {
      setTimeout( $.exceptionLogger( function() {
        // if it's the current window, preserve history
        if( ! context || context === window.self ) {
          window.location = url;
        // if it's an ancestor frame, preserve history there
        } else if( $.contextIsAncestor( context ) ) {
          context.location = url;
        // if it's another context, overwrite the temporary document
        } else {
          context.location.replace( url );
        }
      } ), 0 );
    }
  }
};

  // initalize!
  viglink.init();

  // clean up global vglnk_self, if present
  try { delete window.vglnk_self; } catch( e ) {}
}( typeof vglnk_self === "undefined" ? "vglnk" : vglnk_self ) );

// plugins which are enabled by default, and should be included w/ the library
  /**
 * Link Affiliation for VigLink
 */

window.vglnk = window.vglnk || {};

/**
 * Define the plugin and receive plugin options from the core library.
 */
window.vglnk.link_affiliation_plugin = { run: function( opts, $, vglnk ) {
  vglnk.opt( "rewrite_original", true );
} };
  /**
 * Modified-click support for VigLink
 */

window.vglnk = window.vglnk || {};

/**
 * Define the plugin and receive plugin options from the core library.
 */
window.vglnk.modified_clicks_plugin = { run: function( opts, $, vglnk ) {
  vglnk.opt( "rewrite_modified", true );
} };

// plugins we'd ultimately like to load on demand
  /**
 * Merchant Search for VigLink
 */

window.vglnk = window.vglnk || {};

/**
 * Define the plugin and receive plugin options from the core library.
 */
window.vglnk.dr_search_box_plugin = { run: function( opts, $, vglnk ) {
  // check for required data
  if( ! opts.key ) {
    return;
  }

  var plugin = {
    /**
     * Initialize the plugin
     */
    init: function() {
      $.each( this.getDRSearchForms(), function( form ) {
        if( $.cache( form, "evented" ) ) {
          return;
        }

        var input = plugin.getInput( form ),

        showWatermark = function() {
          if( ! input.value ) {
            $.css( input, { "background-image": "url(http://cdn.viglink.com/images/ebay_watermark.gif)" } );
          }
        };

        $.cache( form, "evented", true );

        // blow away the old (DR) event handlers
        form.onsubmit = null;
        input.onfocus = null;
        input.onblur = null;

        // handle showing/hiding the ebay image
        $.on( input, "focus", function() {
          $.css( input, { "background-image": "none" } );
        } );
        $.on( input, "blur", showWatermark );
        showWatermark();

        // listen for submit
        $.on( form, "submit", function( e ) {
          plugin.onSubmit( e, form );
        } );
      } );
    },

    /**
     * get a specific set of form elements
     */
    getDRSearchForms: function() {
      var forms = [];

      $.each( document.getElementsByTagName( "form" ), function( form ) {
        if( plugin.getInput( form ) && form.id.match( /^DR-ebay-search(CSS|2)?$/i ) ) {
          forms.push( form );
        }
      } );

      return forms;
    },

    /**
     * get the query input field
     */
    getInput: function( form ) {
      return form.p || form.q2;
    },

    /**
     * Handle form submission
     */
    onSubmit: function( e, form ) {
      e = e || window.event;

      var href = "http://shop.ebay.com/i.html?" + $.toQuery( { "_nkw": plugin.getInput( form ).value } ),
          a = $.createA( href, "_blank" );

      $.cache( a, "params", { key: opts.key } );

      vglnk.click( a );

      return $.preventDefault( e );
    }
  };

  plugin.init();
  $.on( "DOMReady", $.bind( plugin.init, plugin ) );
} };
  /**
 * VigLink Page Harmony
 *
 * The library's "harmony level" determines the amount of effort taken to
 * cooperate with other scripts that might appear on the publisher's site.
 * Harmony level can be adjusted in either direction to increase harmony with
 * the rest of the page (positive values), or decrease harmony (be more
 * aggressive; negative values)
 *
 * Specifically, adjusting the harmony level has these effects:
 *
 * 1: Abstain from handling clicks in quirks mode.
 *    https://www.pivotaltracker.com/story/show/20858683
 * 0: (default library behavior)
 * -1: Handle clicks at the link-level, not document-level
 *     https://www.pivotaltracker.com/story/show/40787801
 */

window.vglnk = window.vglnk || {};

/**
 * Define the plugin and receive plugin options from the core library.
 */
window.vglnk.page_harmony_plugin = { run: function( opts, $, vglnk ) {
  var level = parseInt( opts.level, 10 );

  if( isFinite( level ) ) {
    vglnk.opt( "harmony_level", level );
  }
} };
  /**
 * Link Insertion for VigLink
 */

window.vglnk = window.vglnk || {};

/**
 * Define the plugin and receive plugin options from the core library.
 */
window.vglnk.product_linker_plugin = ( function() {
  var

  // standard plugin env "globals"
  opts, $, vglnk, log, platform, plugin,

  /* "enums" */
  INSERTION_TYPE = { LITE: 1, HEAVY: 2, BOTH: 3 },
  LINK_TYPE = { LITE: "L", HEAVY: "H", URL: "U", SEARCH: "S" },

  // experiment data, passed down by the insert call and passed back later
  exp = null,

  // phrase -> { url: ..., count: ... }  hash
  phrases = {},
  // regexp to match any eligible phrase in a string
  phrases_regexp,
  // insertion stats for analytics
  stats = {
    num_inserted: 0,
    num_lite: 0,
    num_heavy: 0,
    num_url: 0
  },

  plugin_api = {
    setup: function( o, u, v, l ) {
      $ = u; vglnk = v; log = l;

      // override default plugin options with those from the server
      opts = $.extend( {
        // API data
        cat: null,       // merchant category
        platform: null,  // aka "pull type"

        // general link insertion options
        api_url: $.updateUrl( vglnk.opt( "api_url" ), { pathname: "/api/insert" } ),
        key: null,
        link_phrases: true,
        link_urls: true,
        link_target: null,
        per_page: null,
        per_phrase: 5,
        product_source: null,
        proximity: null,
        same_proximity: 100,
        scope: null,
        type: INSERTION_TYPE.LITE,
        ui: true
      }, o );

      // check for required data
      if( ! ( opts.api_url && opts.key &&
            ( ( opts.platform && opts.type === INSERTION_TYPE.HEAVY ) ||
              opts.type === INSERTION_TYPE.LITE ||
              opts.type === INSERTION_TYPE.BOTH ) ) ) {
        plugin_api.run = $.noop;
        return;
      }

      // extend $ a bit
      $ = $.extend( $, {
        /**
         * Create a wrapper element for some content, ensuring the content is not
         * modified. (If not for IE <=6, this would be a simple .innerHTML assignment.)
         */
        createWrapper: function( content ) {
          var inner, wrapper = $.createEl( 'div' );

          // IE <= 6 will collapse whitespace, including stripping leading space
          // on assignment to .innerHTML.  Using .outerHTML is more complex, but
          // doesn't have the whitespace bug.
          if( wrapper.outerHTML ) {
            inner = $.createEl( 'div' );
            wrapper.appendChild( inner );
            inner.outerHTML = "<pre>" + content + "</pre>";
            return wrapper.childNodes[0];
          } else {
            wrapper.innerHTML = content;
            return wrapper;
          }
        },

        // alias Sizzle as a utility method
        select: window.vglnk.Sizzle || function(){ return []; }
      } );

      // platform config
      platform = {
        bbp: {
          id_finder: ["li", /^post-(\d+)$/],
          scope: "li .post"
        },
        hdlr: {
          id_finder: ["div", /^post_(\d+)$/],
          scope: ".post-content-area"
        },
        ipb: {
          id_finder: ["div", /^post_id_(\d+)$/],
          scope: ".post_body .post"
        },
        phpb: {
          id_finder: ["div.post", /^p(\d+)$/],
          scope: ".postbody .content, .postbody .signature"
        },
        ubb: {
          id_finder: ["a", /^Post(\d+)$/, "name"],
          scope: ".post_inner *[id^='body'], .post_inner .signature"
        },
        vb3: {
          id_finder: ["td", /^td_post_(\d+)$/],
          scope: [
            "td[id^='td_post_'] div[id^='post_message_']", // post body
            "td[id^='td_post_'] div[id^='post_message_'] ~ div:not([class])" // signature
          ].join( "," )
        },
        vb4: {
          id_finder: ["li", /^post_(\d+)$/],
          scope: ".postbody .content, .postbody .signature, .post-content, ul.conversation-list .list-item-body"
        },
        wppr: {
          id_finder: ["div", /^post-(\d+)$/],
          scope: null
        }
      }[opts.platform] || {};

      plugin = {
        /**
         * Add phrases to the matcher regexp.
         */
        addPhrasesToMatcher: function( new_phrases ) {
          var new_regexp = "";

          if( $.type( new_phrases ) === "regexp" ) {
            new_regexp = new_phrases.source;
          } else if( $.type( new_phrases ) === "array" ) {
            new_regexp = '(?:^|[\\s"\'\\(])(' + $.map( new_phrases, $.escapeRegExp ).join( '|' ) + ')(?=\\s|\\W*$|\\W{2})';
          }

          if( new_regexp !== "" ) {
            phrases_regexp = new RegExp( phrases_regexp ? ( '(?:' + new_regexp + '|' + phrases_regexp.source + ')' ) : new_regexp, 'i' );
          }
        },

        /**
         * Ensure a link is in the viewport.  (Used by the bookmarklet.)
         */
        focusLink: function( link ) {
          if( ! link.id ) {
            link.id = $.uniqid( "vl-link-" );
          }

          location.href.hash = "#" + link.id;
          window.scrollBy( 0, -150 );
        },

        /**
         * Request the list of phrases from the server
         */
        getPhrases: function( post_ids ) {
          $.request( opts.api_url, $.extend( plugin.getPartnerParams(), {
            cat: opts.cat,
            key: opts.key,
            mode: opts.mode,
            pt: opts.platform,
            ps: opts.product_source,
            i: post_ids ? post_ids.join( "|" ) : null,
            u: location.href,
            type: opts.type
          } ), {
            fn: $.bind( plugin.onInsertApi, plugin )
          } );
        },

        /**
         * Get partner params for some API call, if any
         */
        getPartnerParams: function() {
          var name, k,
              partners = vglnk.opt( "partner" ), params = {};

          // there should only be one key, so use the first
          for( name in partners ) { break; }
          if( name ) {
            for( k in partners[name] ) {
              params[name + "_" + k] = partners[name][k];
            }
          }

          return params;
        },

        /**
         * Get the list of post IDs for this page
         */
        getPostIDs: function() {
          var ids = [], matcher = platform ? platform.id_finder : null;

          if( matcher ) {
            $.each( $.select( matcher[0] ), function( el ) {
              var attr = matcher[2] || "id",
                  match = el[attr] ? el[attr].match( matcher[1] ) : null;

              if( match ) {
                ids.push( match[1] );
              }
            } );
          }

          return ids.length ? ids : null;
        },

        /**
         * Initialize an inserted link
         */
        initLink: function( a, data, default_target ) {
          var target = opts.link_target || default_target;
          if( target ) {
            a.target = target;
          }
          a.rel = "nofollow";
          if( ! a.href ) {
            a.href = data.url;
          }

          // set some plugin-specific click params
          $.cache( a, "params", {
            exp: exp,
            key: opts.key,
            merchant: data.merchant,
            mid: data.mid,
            type: data.type || null
          } );

          // enable rewriting
          vglnk.link( a );
        },

        /**
         * Link phrases
         */
        insertLinks: ( function() {
          var ready = {
            timing: false,
            data: opts.link_phrases ? false : opts.link_urls
          },

          /**
           * Link a text phrase to a URL.
           */
          makeLink = function( phrase, data ) {
            var a;

            a = $.createEl( "a" );
            a.innerHTML = phrase.replace( /([a-z0-9]+ *|[^a-z0-9]+)/ig, "<span>$1</span>" );
            a.className = "vglnk";
            $.cache( a, "type", "inserted" );
            $.cache( a, "phrase", phrase );

            if( opts.ui ) {
              a.title = 'Link added by VigLink';
            }

            // enable rewriting, etc.
            plugin.initLink( a, data );

            return a;
          },

          /**
           * Link found phrases in a given text node.
           */
          linkMatches = function( node, fn ) {
            /*
             * Keep in mind the complex case when modifying this function: one text
             * node with multiple phrases, the first of which should not be linked
             * (because of per-phrase link limits).
             */
            var data, match, match_data, link_el, link_text, next_node, offset,

            passesLimitTest = function( data ) {
              var under_phrase_limit, under_page_limit;

              under_phrase_limit = ! data || ( ! opts.per_phrase || ! data.count || data.count < opts.per_phrase );
              under_page_limit = ! opts.per_page || stats.num_inserted < opts.per_page;

              return under_phrase_limit && under_page_limit;
            },

            // TODO optimize
            passesProximityTest = function( node, data ) {
              var i, ii, j, jj,
                  link_segments, phrase, phrase_links, threshold, wrapper, wrapper_geometry,

              cleanup = function( return_value ) {
                wrapper.parentNode.insertBefore( node, wrapper );
                wrapper.parentNode.removeChild( wrapper );

                return return_value;
              },

              regionsAreNearby = function( a, b, threshold ) {
                // pad one region by <threshold> all the way around
                a.x1 -= threshold;
                a.y1 -= threshold;
                a.x2 += threshold;
                a.y2 += threshold;

                // test for overlap
                // (explanation: http://stackoverflow.com/a/306332/53827)
                return (
                  a.x1 < b.x2 && a.x2 > b.x1 &&
                  a.y1 < b.y2 && a.y2 > b.y1
                );
              };

              // wrap the text node in a span so we can get its position
              wrapper = $.createEl( "span" );
              node.parentNode.insertBefore( wrapper, node );
              wrapper.appendChild( node );
              wrapper_geometry = $.geometry( wrapper );

              // test for proximity to any other inserted link
              if( opts.proximity || opts.same_proximity ) {
                for( phrase in phrases ) {
                  phrase_links = phrases[phrase].links;
                  if( phrase_links ) {
                    // skip if there's no restriction for this kind of link
                    if( phrases[phrase] === data && ! opts.same_proximity ||
                        phrases[phrase] !== data && ! opts.proximity ) {
                      continue;
                    }

                    // set threshold for this kind of link
                    if( phrases[phrase] === data ) {
                      threshold = Math.max( opts.same_proximity, opts.proximity );
                      // threshold = opts.same_proximity;
                    } else {
                      threshold = opts.proximity;
                    }

                    // test against each previously inserted link segment
                    for( i = 0, ii = phrase_links.length; i < ii; i++ ) {
                      link_segments = phrase_links[i].segments;
                      for( j = 0, jj = link_segments.length; j < jj; j++ ) {
                        if( regionsAreNearby( link_segments[j].geometry, wrapper_geometry, threshold ) ) {
                          return cleanup( false );
                        }
                      }
                    }
                  }
                }
              }

              return cleanup( true );
            },

            saveLinkToData = function( link_el, data ) {
              var i, ii, current_span, last_span,
                link_data = { el: link_el, segments: [] },
                link_spans = link_el.getElementsByTagName( "span" ),
                segment = { els: [] };

              // find link segments (seperate chunks created by text wrapping)
              for( i = 0, ii = link_spans.length; i < ii; i++ ) {
                current_span = link_spans[i];

                if( last_span === undefined || current_span.offsetTop === last_span.offsetTop ) {
                  segment.els.push( current_span );
                } else {
                  link_data.segments.push( segment );
                  segment = { els: [current_span] };
                }

                last_span = current_span;
              }
              segment.geometry = $.geometry.apply( $, segment.els );
              link_data.segments.push( segment );

              data.links = data.links || [];
              data.links.push( link_data );

              return data;
            };

            while( node && node.data && Boolean( match_data = node.data.match( phrases_regexp ) ) ) {
              match = match_data[0];
              link_text = match_data.slice( 1 ).join( '' );
              data = phrases[link_text.toLowerCase()];
              offset = node.data.indexOf( match ) + match.length - link_text.length;

              // skip up to the match
              if( offset > 0 ) {
                node = node.splitText( offset );
              }

              // skip past the match for the next iteration (avoiding an IE bug when splitting at the end of the node)
              if( node.length <= link_text.length ) {
                next_node = null;
              } else {
                next_node = node.splitText( link_text.length );
              }

              // link a match?
              if( link_text && passesLimitTest( data ) && passesProximityTest( node, data ) ) {
                // figure out what kind of link this is and count it for analytics
                if( ! data ) {
                  data = {
                    url: link_text.match( /^https?:\/\//i ) ? link_text : ( "http://" + link_text ),
                    type: LINK_TYPE.URL
                  };
                  stats.num_url++;
                } else if( data.type.toUpperCase() === LINK_TYPE.LITE ) {
                  stats.num_lite++;
                } else {
                  stats.num_heavy++;
                }

                link_el = makeLink( link_text, data );
                fn( link_el, node );
                saveLinkToData( link_el, data );

                data.count++;
                stats.num_inserted++;
              }

              node = next_node;
            }
          },

          /**
           * Find and link all phrases in a text node.
           */
          findAndLinkPhrases = function( text_node ) {
            var has_matches = phrases_regexp.test( text_node.data ),
                parent = text_node.parentNode,

            replaceTextWithLink = function( link_el, text_node ) {
              parent.insertBefore( link_el, text_node );
              parent.removeChild( text_node );
            };

            if( has_matches ) {
              linkMatches( text_node, replaceTextWithLink );
            }
          },

          _insertLinks = function( start_node ) {
            var next_node, node, excluded_tags = ',' + [
              // duh
              'a',
              // text content might be rendered, but it's infrequent and worth skipping
              'applet', 'embed', 'object',
              // text content might be rendered, but that probably wasn't the intention
              'head', 'img', 'input', 'link', 'map', 'meta', 'param', 'select',
              // text content is not rendered as inline text
              'button', 'iframe', 'option', 'script', 'style', 'svg', 'textarea', 'title'
            ].join( ',' ) + ',',

            excludedByClassName = function( node ) {
              return $.hasClass( node, "nolinks" ) ||
                $.hasClass( node, "atma-nolink" ) ||
                $.hasClass( node, "atma-no-link" );
            },

            excludedByTagName = function( node ) {
              return excluded_tags.indexOf( ',' + node.nodeName.toLowerCase() + ',' ) !== -1;
            },

            excludedNode = function( node ) {
              return excludedByTagName( node ) || excludedByClassName( node );
            },

            excludedByAncestor = function( node ) {
              while( node.parentNode ) {
                node = node.parentNode;
                if( node.nodeType === 1 && excludedNode( node ) ) {
                  return true;
                }
              }
            };

            if( ! start_node ) {
              $.each( $.select( opts.scope ), function( node ) {
                // we're starting in the middle of the DOM. make sure ancestors aren't excluded
                if( ! excludedByAncestor( node ) ) {
                  _insertLinks( node );
                }
              } );
              return;
            } else if( excludedNode( start_node ) ) {
              return;
            }

            node = start_node.firstChild;
            while( node ) {
              next_node = node.nextSibling;

              // allowed element? descend into its children
              if( node.nodeType === 1 ) {
                _insertLinks( node );
              // matching text node? do replacement
              } else if( node.nodeType === 3 ) {
                findAndLinkPhrases( node );
              }

              node = next_node;
            }
          };

          return function() {
            ready = $.extend( ready, arguments[0] );

            if( ready.timing && ready.data && phrases_regexp ) {
              _insertLinks();
              plugin.log();
            }
          };
        }() ),

        /**
         * Load an array of phrases
         */
        loadPhrases: function( response ) {
          var i, ii, link, new_phrases = [];

          // convert the phrase array to a hash
          for( i = 0, ii = response.results.length; i < ii; i++ ) {
            link = response.results[i];

            // check for required data
            if( link.phrase && link.url ) {
              link.phrase = link.phrase.toLowerCase();
              if( ! phrases[link.phrase] ) {
                new_phrases.push( link.phrase );
                phrases[link.phrase] = {
                  count: phrases[link.phrase] ? phrases[link.phrase].count : 0,
                  phrase: link.phrase,
                  merchant: link.merchant,
                  mid: link.mid,
                  url: link.url,
                  type: link.type || ""
                };
              }
            }
          }

          if( new_phrases.length > 0 ) {
            this.addPhrasesToMatcher( new_phrases );
          }

          this.insertLinks( { data: true } );
        },

        /**
         * Log some insertion stats to the server
         */
        log: function() {
          var i, count, pair, sets = {
            heavy: [],
            lite: []
          },

          sortByCount = function(a, b) {
            return b.split( "," )[0] - a.split( "," )[0];
          };

          for( i in phrases ) {
            count = parseInt( phrases[i].count, 10 );
            if( count > 0 ) {
              pair = [count, i.replace( /\|/g, '%7C' )].join( "," );
              sets[phrases[i].type.toUpperCase() === LINK_TYPE.LITE ? "lite" : "heavy"].push( pair );
            }
          }

          sets.lite.sort( sortByCount );
          sets.heavy.sort( sortByCount );

          log( "insert", [ {
            ct: stats.num_inserted,
            cl: stats.num_lite,
            ch: stats.num_heavy,
            cu: stats.num_url,
            exp: exp
          }, {
            pl: sets.lite.join( "|" )
          }, {
            ph: sets.heavy.join( "|" )
          } ] );
        },

        /**
         * Handle the insertion API response.
         */
        onInsertApi: function( response ) {
          if( $.type( response ) === "object" && response.results ) {
            exp = response.exp;
            this.loadPhrases( response );
          }
        },

        /**
         * initialize link insertion
         */
        setup: function() {
          var url_regex, getPhrases = $.bind( function() {
            this.getPhrases( this.getPostIDs() );
          }, this );

          // set default scope
          opts.scope = opts.scope || platform.scope || "body";

          if( opts.link_urls ) {
            // regular expression to match http urls or partial, probable web urls
            //   based on regexp from http://daringfireball.net/2010/07/improved_regex_for_matching_urls
            url_regex = ( function() {
              var r = {
                http_scheme:     "https?://",
                www_domain_part: "www\\d{0,3}\\.",
                domain_parts:    "(?:[a-z0-9-]+\\.)+",
                tld:             "[a-z]{2,4}",

                nonparen_path_char: "[^\\s()<>]",

                hard_start: "(?:^|\\s)\\W*",
                soft_start: "\\b"
              };
              r.balanced_parens = "\\((?:" + r.nonparen_path_char + "|(?:\\(" + r.nonparen_path_char + "+\\)))*\\)";

              return "(?:" +
                // a probable URL
                "(?:" + r.soft_start + "(" + r.http_scheme + ")|" + r.hard_start + "(" + r.www_domain_part + "|" + r.domain_parts + r.tld + "/))" +
                "((?:" + r.nonparen_path_char + "+|" + r.balanced_parens + ")+" +
                "(?:" + r.balanced_parens + "|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))" +
                // or a .com domain alone
                "|" + r.hard_start + "(" + r.domain_parts + "com(?:/|\\b))" +
              ")";
            }() );

            this.addPhrasesToMatcher( new RegExp( url_regex, "i" ) );
          }

          if( opts.link_phrases ) {
            if( ! opts.platform || opts.platform === "full" ) {
              getPhrases();
            } else {
              // if platform is set we need to scrape post IDs
              $.on( "DOMReady", getPhrases );
            }
          }
        }
      };

      plugin.setup();
    },

    run: function() {
      $.on( "DOMReady", function() {
        plugin.insertLinks( { timing: true } );
      } );
    }
  };

  return plugin_api;
}() );

// third-party libraries
  // Sizzle is a dependency of plugins/product_linker.js
  /**
 *  @preserve
 *  Includes:
 *
 *  Sizzle CSS Selector Engine
 *  Copyright 2011, The Dojo Foundation
 *  Released under the MIT, BSD, and GPL Licenses.
 *  More information: http://sizzlejs.com/
 */
(function(){

var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
	done = 0,
	toString = Object.prototype.toString,
	hasDuplicate = false,
	baseHasDuplicate = true;

// Here we check if the JavaScript engine is using some sort of
// optimization where it does not always call our comparision
// function. If that is the case, discard the hasDuplicate value.
//   Thus far that includes Google Chrome.
[0, 0].sort(function() {
	baseHasDuplicate = false;
	return 0;
});

var Sizzle = function( selector, context, results, seed ) {
	results = results || [];
	context = context || document;

	var origContext = context;

	if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
		return [];
	}
	
	if ( !selector || typeof selector !== "string" ) {
		return results;
	}

	var m, set, checkSet, extra, ret, cur, pop, i,
		prune = true,
		contextXML = Sizzle.isXML( context ),
		parts = [],
		soFar = selector;
	
	// Reset the position of the chunker regexp (start from head)
	do {
		chunker.exec( "" );
		m = chunker.exec( soFar );

		if ( m ) {
			soFar = m[3];
		
			parts.push( m[1] );
		
			if ( m[2] ) {
				extra = m[3];
				break;
			}
		}
	} while ( m );

	if ( parts.length > 1 && origPOS.exec( selector ) ) {

		if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
			set = posProcess( parts[0] + parts[1], context );

		} else {
			set = Expr.relative[ parts[0] ] ?
				[ context ] :
				Sizzle( parts.shift(), context );

			while ( parts.length ) {
				selector = parts.shift();

				if ( Expr.relative[ selector ] ) {
					selector += parts.shift();
				}
				
				set = posProcess( selector, set );
			}
		}

	} else {
		// Take a shortcut and set the context if the root selector is an ID
		// (but not if it'll be faster if the inner selector is an ID)
		if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
				Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {

			ret = Sizzle.find( parts.shift(), context, contextXML );
			context = ret.expr ?
				Sizzle.filter( ret.expr, ret.set )[0] :
				ret.set[0];
		}

		if ( context ) {
			ret = seed ?
				{ expr: parts.pop(), set: makeArray(seed) } :
				Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );

			set = ret.expr ?
				Sizzle.filter( ret.expr, ret.set ) :
				ret.set;

			if ( parts.length > 0 ) {
				checkSet = makeArray( set );

			} else {
				prune = false;
			}

			while ( parts.length ) {
				cur = parts.pop();
				pop = cur;

				if ( !Expr.relative[ cur ] ) {
					cur = "";
				} else {
					pop = parts.pop();
				}

				if ( pop == null ) {
					pop = context;
				}

				Expr.relative[ cur ]( checkSet, pop, contextXML );
			}

		} else {
			checkSet = parts = [];
		}
	}

	if ( !checkSet ) {
		checkSet = set;
	}

	if ( !checkSet ) {
		Sizzle.error( cur || selector );
	}

	if ( toString.call(checkSet) === "[object Array]" ) {
		if ( !prune ) {
			results.push.apply( results, checkSet );

		} else if ( context && context.nodeType === 1 ) {
			for ( i = 0; checkSet[i] != null; i++ ) {
				if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
					results.push( set[i] );
				}
			}

		} else {
			for ( i = 0; checkSet[i] != null; i++ ) {
				if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
					results.push( set[i] );
				}
			}
		}

	} else {
		makeArray( checkSet, results );
	}

	if ( extra ) {
		Sizzle( extra, origContext, results, seed );
		Sizzle.uniqueSort( results );
	}

	return results;
};

Sizzle.uniqueSort = function( results ) {
	if ( sortOrder ) {
		hasDuplicate = baseHasDuplicate;
		results.sort( sortOrder );

		if ( hasDuplicate ) {
			for ( var i = 1; i < results.length; i++ ) {
				if ( results[i] === results[ i - 1 ] ) {
					results.splice( i--, 1 );
				}
			}
		}
	}

	return results;
};

Sizzle.matches = function( expr, set ) {
	return Sizzle( expr, null, null, set );
};

Sizzle.matchesSelector = function( node, expr ) {
	return Sizzle( expr, null, null, [node] ).length > 0;
};

Sizzle.find = function( expr, context, isXML ) {
	var set;

	if ( !expr ) {
		return [];
	}

	for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
		var match,
			type = Expr.order[i];
		
		if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
			var left = match[1];
			match.splice( 1, 1 );

			if ( left.substr( left.length - 1 ) !== "\\" ) {
				match[1] = (match[1] || "").replace(/\\/g, "");
				set = Expr.find[ type ]( match, context, isXML );

				if ( set != null ) {
					expr = expr.replace( Expr.match[ type ], "" );
					break;
				}
			}
		}
	}

	if ( !set ) {
		set = typeof context.getElementsByTagName !== "undefined" ?
			context.getElementsByTagName( "*" ) :
			[];
	}

	return { set: set, expr: expr };
};

Sizzle.filter = function( expr, set, inplace, not ) {
	var match, anyFound,
		old = expr,
		result = [],
		curLoop = set,
		isXMLFilter = set && set[0] && Sizzle.isXML( set[0] );

	while ( expr && set.length ) {
		for ( var type in Expr.filter ) {
			if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
				var found, item,
					filter = Expr.filter[ type ],
					left = match[1];

				anyFound = false;

				match.splice(1,1);

				if ( left.substr( left.length - 1 ) === "\\" ) {
					continue;
				}

				if ( curLoop === result ) {
					result = [];
				}

				if ( Expr.preFilter[ type ] ) {
					match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );

					if ( !match ) {
						anyFound = found = true;

					} else if ( match === true ) {
						continue;
					}
				}

				if ( match ) {
					for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
						if ( item ) {
							found = filter( item, match, i, curLoop );
							var pass = not ^ !!found;

							if ( inplace && found != null ) {
								if ( pass ) {
									anyFound = true;

								} else {
									curLoop[i] = false;
								}

							} else if ( pass ) {
								result.push( item );
								anyFound = true;
							}
						}
					}
				}

				if ( found !== undefined ) {
					if ( !inplace ) {
						curLoop = result;
					}

					expr = expr.replace( Expr.match[ type ], "" );

					if ( !anyFound ) {
						return [];
					}

					break;
				}
			}
		}

		// Improper expression
		if ( expr === old ) {
			if ( anyFound == null ) {
				Sizzle.error( expr );

			} else {
				break;
			}
		}

		old = expr;
	}

	return curLoop;
};

Sizzle.error = function( msg ) {
	throw "Syntax error, unrecognized expression: " + msg;
};

var Expr = Sizzle.selectors = {
	order: [ "ID", "NAME", "TAG" ],

	match: {
		ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
		CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
		NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,
		ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,
		TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,
		CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,
		POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,
		PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
	},

	leftMatch: {},

	attrMap: {
		"class": "className",
		"for": "htmlFor"
	},

	attrHandle: {
		href: function( elem ) {
			return elem.getAttribute( "href" );
		}
	},

	relative: {
		"+": function(checkSet, part){
			var isPartStr = typeof part === "string",
				isTag = isPartStr && !/\W/.test( part ),
				isPartStrNotTag = isPartStr && !isTag;

			if ( isTag ) {
				part = part.toLowerCase();
			}

			for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
				if ( (elem = checkSet[i]) ) {
					while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}

					checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
						elem || false :
						elem === part;
				}
			}

			if ( isPartStrNotTag ) {
				Sizzle.filter( part, checkSet, true );
			}
		},

		">": function( checkSet, part ) {
			var elem,
				isPartStr = typeof part === "string",
				i = 0,
				l = checkSet.length;

			if ( isPartStr && !/\W/.test( part ) ) {
				part = part.toLowerCase();

				for ( ; i < l; i++ ) {
					elem = checkSet[i];

					if ( elem ) {
						var parent = elem.parentNode;
						checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
					}
				}

			} else {
				for ( ; i < l; i++ ) {
					elem = checkSet[i];

					if ( elem ) {
						checkSet[i] = isPartStr ?
							elem.parentNode :
							elem.parentNode === part;
					}
				}

				if ( isPartStr ) {
					Sizzle.filter( part, checkSet, true );
				}
			}
		},

		"": function(checkSet, part, isXML){
			var nodeCheck,
				doneName = done++,
				checkFn = dirCheck;

			if ( typeof part === "string" && !/\W/.test(part) ) {
				part = part.toLowerCase();
				nodeCheck = part;
				checkFn = dirNodeCheck;
			}

			checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML );
		},

		"~": function( checkSet, part, isXML ) {
			var nodeCheck,
				doneName = done++,
				checkFn = dirCheck;

			if ( typeof part === "string" && !/\W/.test( part ) ) {
				part = part.toLowerCase();
				nodeCheck = part;
				checkFn = dirNodeCheck;
			}

			checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML );
		}
	},

	find: {
		ID: function( match, context, isXML ) {
			if ( typeof context.getElementById !== "undefined" && !isXML ) {
				var m = context.getElementById(match[1]);
				// Check parentNode to catch when Blackberry 4.6 returns
				// nodes that are no longer in the document #6963
				return m && m.parentNode ? [m] : [];
			}
		},

		NAME: function( match, context ) {
			if ( typeof context.getElementsByName !== "undefined" ) {
				var ret = [],
					results = context.getElementsByName( match[1] );

				for ( var i = 0, l = results.length; i < l; i++ ) {
					if ( results[i].getAttribute("name") === match[1] ) {
						ret.push( results[i] );
					}
				}

				return ret.length === 0 ? null : ret;
			}
		},

		TAG: function( match, context ) {
			if ( typeof context.getElementsByTagName !== "undefined" ) {
				return context.getElementsByTagName( match[1] );
			}
		}
	},
	preFilter: {
		CLASS: function( match, curLoop, inplace, result, not, isXML ) {
			match = " " + match[1].replace(/\\/g, "") + " ";

			if ( isXML ) {
				return match;
			}

			for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
				if ( elem ) {
					if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) {
						if ( !inplace ) {
							result.push( elem );
						}

					} else if ( inplace ) {
						curLoop[i] = false;
					}
				}
			}

			return false;
		},

		ID: function( match ) {
			return match[1].replace(/\\/g, "");
		},

		TAG: function( match, curLoop ) {
			return match[1].toLowerCase();
		},

		CHILD: function( match ) {
			if ( match[1] === "nth" ) {
				if ( !match[2] ) {
					Sizzle.error( match[0] );
				}

				match[2] = match[2].replace(/^\+|\s*/g, '');

				// parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
				var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec(
					match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
					!/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);

				// calculate the numbers (first)n+(last) including if they are negative
				match[2] = (test[1] + (test[2] || 1)) - 0;
				match[3] = test[3] - 0;
			}
			else if ( match[2] ) {
				Sizzle.error( match[0] );
			}

			// TODO: Move to normal caching system
			match[0] = done++;

			return match;
		},

		ATTR: function( match, curLoop, inplace, result, not, isXML ) {
			var name = match[1] = match[1].replace(/\\/g, "");
			
			if ( !isXML && Expr.attrMap[name] ) {
				match[1] = Expr.attrMap[name];
			}

			// Handle if an un-quoted value was used
			match[4] = ( match[4] || match[5] || "" ).replace(/\\/g, "");

			if ( match[2] === "~=" ) {
				match[4] = " " + match[4] + " ";
			}

			return match;
		},

		PSEUDO: function( match, curLoop, inplace, result, not ) {
			if ( match[1] === "not" ) {
				// If we're dealing with a complex expression, or a simple one
				if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
					match[3] = Sizzle(match[3], null, null, curLoop);

				} else {
					var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);

					if ( !inplace ) {
						result.push.apply( result, ret );
					}

					return false;
				}

			} else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
				return true;
			}
			
			return match;
		},

		POS: function( match ) {
			match.unshift( true );

			return match;
		}
	},
	
	filters: {
		enabled: function( elem ) {
			return elem.disabled === false && elem.type !== "hidden";
		},

		disabled: function( elem ) {
			return elem.disabled === true;
		},

		checked: function( elem ) {
			return elem.checked === true;
		},
		
		selected: function( elem ) {
			// Accessing this property makes selected-by-default
			// options in Safari work properly
  		/** @nosideeffects */
			elem.parentNode.selectedIndex;
			
			return elem.selected === true;
		},

		parent: function( elem ) {
			return !!elem.firstChild;
		},

		empty: function( elem ) {
			return !elem.firstChild;
		},

		has: function( elem, i, match ) {
			return !!Sizzle( match[3], elem ).length;
		},

		header: function( elem ) {
			return (/h\d/i).test( elem.nodeName );
		},

		text: function( elem ) {
			return "text" === elem.type;
		},
		radio: function( elem ) {
			return "radio" === elem.type;
		},

		checkbox: function( elem ) {
			return "checkbox" === elem.type;
		},

		file: function( elem ) {
			return "file" === elem.type;
		},
		password: function( elem ) {
			return "password" === elem.type;
		},

		submit: function( elem ) {
			return "submit" === elem.type;
		},

		image: function( elem ) {
			return "image" === elem.type;
		},

		reset: function( elem ) {
			return "reset" === elem.type;
		},

		button: function( elem ) {
			return "button" === elem.type || elem.nodeName.toLowerCase() === "button";
		},

		input: function( elem ) {
			return (/input|select|textarea|button/i).test( elem.nodeName );
		}
	},
	setFilters: {
		first: function( elem, i ) {
			return i === 0;
		},

		last: function( elem, i, match, array ) {
			return i === array.length - 1;
		},

		even: function( elem, i ) {
			return i % 2 === 0;
		},

		odd: function( elem, i ) {
			return i % 2 === 1;
		},

		lt: function( elem, i, match ) {
			return i < match[3] - 0;
		},

		gt: function( elem, i, match ) {
			return i > match[3] - 0;
		},

		nth: function( elem, i, match ) {
			return match[3] - 0 === i;
		},

		eq: function( elem, i, match ) {
			return match[3] - 0 === i;
		}
	},
	filter: {
		PSEUDO: function( elem, match, i, array ) {
			var name = match[1],
				filter = Expr.filters[ name ];

			if ( filter ) {
				return filter( elem, i, match, array );

			} else if ( name === "contains" ) {
				return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0;

			} else if ( name === "not" ) {
				var not = match[3];

				for ( var j = 0, l = not.length; j < l; j++ ) {
					if ( not[j] === elem ) {
						return false;
					}
				}

				return true;

			} else {
				Sizzle.error( name );
			}
		},

		CHILD: function( elem, match ) {
			var type = match[1],
				node = elem;

			switch ( type ) {
				case "only":
				case "first":
					while ( (node = node.previousSibling) )	 {
						if ( node.nodeType === 1 ) { 
							return false; 
						}
					}

					if ( type === "first" ) { 
						return true; 
					}

					node = elem;

				case "last":
					while ( (node = node.nextSibling) )	 {
						if ( node.nodeType === 1 ) { 
							return false; 
						}
					}

					return true;

				case "nth":
					var first = match[2],
						last = match[3];

					if ( first === 1 && last === 0 ) {
						return true;
					}
					
					var doneName = match[0],
						parent = elem.parentNode;
	
					if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) {
						var count = 0;
						
						for ( node = parent.firstChild; node; node = node.nextSibling ) {
							if ( node.nodeType === 1 ) {
								node.nodeIndex = ++count;
							}
						} 

						parent.sizcache = doneName;
					}
					
					var diff = elem.nodeIndex - last;

					if ( first === 0 ) {
						return diff === 0;

					} else {
						return ( diff % first === 0 && diff / first >= 0 );
					}
			}
		},

		ID: function( elem, match ) {
			return elem.nodeType === 1 && elem.getAttribute("id") === match;
		},

		TAG: function( elem, match ) {
			return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match;
		},
		
		CLASS: function( elem, match ) {
			return (" " + (elem.className || elem.getAttribute("class")) + " ")
				.indexOf( match ) > -1;
		},

		ATTR: function( elem, match ) {
			var name = match[1],
				result = Expr.attrHandle[ name ] ?
					Expr.attrHandle[ name ]( elem ) :
					elem[ name ] != null ?
						elem[ name ] :
						elem.getAttribute( name ),
				value = result + "",
				type = match[2],
				check = match[4];

			return result == null ?
				type === "!=" :
				type === "=" ?
				value === check :
				type === "*=" ?
				value.indexOf(check) >= 0 :
				type === "~=" ?
				(" " + value + " ").indexOf(check) >= 0 :
				!check ?
				value && result !== false :
				type === "!=" ?
				value !== check :
				type === "^=" ?
				value.indexOf(check) === 0 :
				type === "$=" ?
				value.substr(value.length - check.length) === check :
				type === "|=" ?
				value === check || value.substr(0, check.length + 1) === check + "-" :
				false;
		},

		POS: function( elem, match, i, array ) {
			var name = match[2],
				filter = Expr.setFilters[ name ];

			if ( filter ) {
				return filter( elem, i, match, array );
			}
		}
	}
};

var origPOS = Expr.match.POS,
	fescape = function(all, num){
		return "\\" + (num - 0 + 1);
	};

for ( var type in Expr.match ) {
	Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) );
	Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) );
}

var makeArray = function( array, results ) {
	array = Array.prototype.slice.call( array, 0 );

	if ( results ) {
		results.push.apply( results, array );
		return results;
	}
	
	return array;
};

// Perform a simple check to determine if the browser is capable of
// converting a NodeList to an array using builtin methods.
// Also verifies that the returned array holds DOM nodes
// (which is not the case in the Blackberry browser)
try {
	var nt = Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType;

// Provide a fallback method if it does not work
} catch( e ) {
	makeArray = function( array, results ) {
		var i = 0,
			ret = results || [];

		if ( toString.call(array) === "[object Array]" ) {
			Array.prototype.push.apply( ret, array );

		} else {
			if ( typeof array.length === "number" ) {
				for ( var l = array.length; i < l; i++ ) {
					ret.push( array[i] );
				}

			} else {
				for ( ; array[i]; i++ ) {
					ret.push( array[i] );
				}
			}
		}

		return ret;
	};
}

var sortOrder, siblingCheck;

if ( document.documentElement.compareDocumentPosition ) {
	sortOrder = function( a, b ) {
		if ( a === b ) {
			hasDuplicate = true;
			return 0;
		}

		if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
			return a.compareDocumentPosition ? -1 : 1;
		}

		return a.compareDocumentPosition(b) & 4 ? -1 : 1;
	};

} else {
	sortOrder = function( a, b ) {
		var al, bl,
			ap = [],
			bp = [],
			aup = a.parentNode,
			bup = b.parentNode,
			cur = aup;

		// The nodes are identical, we can exit early
		if ( a === b ) {
			hasDuplicate = true;
			return 0;

		// If the nodes are siblings (or identical) we can do a quick check
		} else if ( aup === bup ) {
			return siblingCheck( a, b );

		// If no parents were found then the nodes are disconnected
		} else if ( !aup ) {
			return -1;

		} else if ( !bup ) {
			return 1;
		}

		// Otherwise they're somewhere else in the tree so we need
		// to build up a full list of the parentNodes for comparison
		while ( cur ) {
			ap.unshift( cur );
			cur = cur.parentNode;
		}

		cur = bup;

		while ( cur ) {
			bp.unshift( cur );
			cur = cur.parentNode;
		}

		al = ap.length;
		bl = bp.length;

		// Start walking down the tree looking for a discrepancy
		for ( var i = 0; i < al && i < bl; i++ ) {
			if ( ap[i] !== bp[i] ) {
				return siblingCheck( ap[i], bp[i] );
			}
		}

		// We ended someplace up the tree so do a sibling check
		return i === al ?
			siblingCheck( a, bp[i], -1 ) :
			siblingCheck( ap[i], b, 1 );
	};

	siblingCheck = function( a, b, ret ) {
		if ( a === b ) {
			return ret;
		}

		var cur = a.nextSibling;

		while ( cur ) {
			if ( cur === b ) {
				return -1;
			}

			cur = cur.nextSibling;
		}

		return 1;
	};
}

// Utility function for retreiving the text value of an array of DOM nodes
Sizzle.getText = function( elems ) {
	var ret = "", elem;

	for ( var i = 0; elems[i]; i++ ) {
		elem = elems[i];

		// Get the text from text nodes and CDATA nodes
		if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
			ret += elem.nodeValue;

		// Traverse everything else, except comment nodes
		} else if ( elem.nodeType !== 8 ) {
			ret += Sizzle.getText( elem.childNodes );
		}
	}

	return ret;
};

// Check to see if the browser returns elements by name when
// querying by getElementById (and provide a workaround)
(function(){
	// We're going to inject a fake input element with a specified name
	var form = document.createElement("div"),
		id = "script" + (new Date()).getTime(),
		root = document.documentElement;

	form.innerHTML = "<a name='" + id + "'/>";

	// Inject it into the root element, check its status, and remove it quickly
	root.insertBefore( form, root.firstChild );

	// The workaround has to do additional checks after a getElementById
	// Which slows things down for other browsers (hence the branching)
	if ( document.getElementById( id ) ) {
		Expr.find.ID = function( match, context, isXML ) {
			if ( typeof context.getElementById !== "undefined" && !isXML ) {
				var m = context.getElementById(match[1]);

				return m ?
					m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ?
						[m] :
						undefined :
					[];
			}
		};

		Expr.filter.ID = function( elem, match ) {
			var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");

			return elem.nodeType === 1 && node && node.nodeValue === match;
		};
	}

	root.removeChild( form );

	// release memory in IE
	root = form = null;
})();

(function(){
	// Check to see if the browser returns only elements
	// when doing getElementsByTagName("*")

	// Create a fake element
	var div = document.createElement("div");
	div.appendChild( document.createComment("") );

	// Make sure no comments are found
	if ( div.getElementsByTagName("*").length > 0 ) {
		Expr.find.TAG = function( match, context ) {
			var results = context.getElementsByTagName( match[1] );

			// Filter out possible comments
			if ( match[1] === "*" ) {
				var tmp = [];

				for ( var i = 0; results[i]; i++ ) {
					if ( results[i].nodeType === 1 ) {
						tmp.push( results[i] );
					}
				}

				results = tmp;
			}

			return results;
		};
	}

	// Check to see if an attribute returns normalized href attributes
	div.innerHTML = "<a href='#'></a>";

	if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
			div.firstChild.getAttribute("href") !== "#" ) {

		Expr.attrHandle.href = function( elem ) {
			return elem.getAttribute( "href", 2 );
		};
	}

	// release memory in IE
	div = null;
})();

if ( document.querySelectorAll ) {
	(function(){
		var oldSizzle = Sizzle,
			div = document.createElement("div"),
			id = "__sizzle__";

		div.innerHTML = "<p class='TEST'></p>";

		// Safari can't handle uppercase or unicode characters when
		// in quirks mode.
		if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
			return;
		}
	
		Sizzle = function( query, context, extra, seed ) {
			context = context || document;

			// Only use querySelectorAll on non-XML documents
			// (ID selectors don't work in non-HTML documents)
			if ( !seed && !Sizzle.isXML(context) ) {
				// See if we find a selector to speed up
				var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query );
				
				if ( match && (context.nodeType === 1 || context.nodeType === 9) ) {
					// Speed-up: Sizzle("TAG")
					if ( match[1] ) {
						return makeArray( context.getElementsByTagName( query ), extra );
					
					// Speed-up: Sizzle(".CLASS")
					} else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) {
						return makeArray( context.getElementsByClassName( match[2] ), extra );
					}
				}
				
				if ( context.nodeType === 9 ) {
					// Speed-up: Sizzle("body")
					// The body element only exists once, optimize finding it
					if ( query === "body" && context.body ) {
						return makeArray( [ context.body ], extra );
						
					// Speed-up: Sizzle("#ID")
					} else if ( match && match[3] ) {
						var elem = context.getElementById( match[3] );

						// Check parentNode to catch when Blackberry 4.6 returns
						// nodes that are no longer in the document #6963
						if ( elem && elem.parentNode ) {
							// Handle the case where IE and Opera return items
							// by name instead of ID
							if ( elem.id === match[3] ) {
								return makeArray( [ elem ], extra );
							}
							
						} else {
							return makeArray( [], extra );
						}
					}
					
					try {
						return makeArray( context.querySelectorAll(query), extra );
					} catch(qsaError) {}

				// qSA works strangely on Element-rooted queries
				// We can work around this by specifying an extra ID on the root
				// and working up from there (Thanks to Andrew Dupont for the technique)
				// IE 8 doesn't work on object elements
				} else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
					var old = context.getAttribute( "id" ),
						nid = old || id,
						hasParent = context.parentNode,
						relativeHierarchySelector = /^\s*[+~]/.test( query );

					if ( !old ) {
						context.setAttribute( "id", nid );
					} else {
						nid = nid.replace( /'/g, "\\$&" );
					}
					if ( relativeHierarchySelector && hasParent ) {
						context = context.parentNode;
					}

					try {
						if ( !relativeHierarchySelector || hasParent ) {
							return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra );
						}

					} catch(pseudoError) {
					} finally {
						if ( !old ) {
							context.removeAttribute( "id" );
						}
					}
				}
			}
		
			return oldSizzle(query, context, extra, seed);
		};

		for ( var prop in oldSizzle ) {
			Sizzle[ prop ] = oldSizzle[ prop ];
		}

		// release memory in IE
		div = null;
	})();
}

(function(){
	var html = document.documentElement,
		matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector,
		pseudoWorks = false;

	try {
		// This should fail with an exception
		// Gecko does not error, returns false instead
		matches.call( document.documentElement, "[test!='']:sizzle" );
	
	} catch( pseudoError ) {
		pseudoWorks = true;
	}

	if ( matches ) {
		Sizzle.matchesSelector = function( node, expr ) {
			// Make sure that attribute selectors are quoted
			expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']");

			if ( !Sizzle.isXML( node ) ) {
				try { 
					if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) {
						return matches.call( node, expr );
					}
				} catch(e) {}
			}

			return Sizzle(expr, null, null, [node]).length > 0;
		};
	}
})();

(function(){
	var div = document.createElement("div");

	div.innerHTML = "<div class='test e'></div><div class='test'></div>";

	// Opera can't find a second classname (in 9.6)
	// Also, make sure that getElementsByClassName actually exists
	if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
		return;
	}

	// Safari caches class attributes, doesn't catch changes (in 3.2)
	div.lastChild.className = "e";

	if ( div.getElementsByClassName("e").length === 1 ) {
		return;
	}
	
	Expr.order.splice(1, 0, "CLASS");
	Expr.find.CLASS = function( match, context, isXML ) {
		if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
			return context.getElementsByClassName(match[1]);
		}
	};

	// release memory in IE
	div = null;
})();

function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
	for ( var i = 0, l = checkSet.length; i < l; i++ ) {
		var elem = checkSet[i];

		if ( elem ) {
			var match = false;

			elem = elem[dir];

			while ( elem ) {
				if ( elem.sizcache === doneName ) {
					match = checkSet[elem.sizset];
					break;
				}

				if ( elem.nodeType === 1 && !isXML ){
					elem.sizcache = doneName;
					elem.sizset = i;
				}

				if ( elem.nodeName.toLowerCase() === cur ) {
					match = elem;
					break;
				}

				elem = elem[dir];
			}

			checkSet[i] = match;
		}
	}
}

function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
	for ( var i = 0, l = checkSet.length; i < l; i++ ) {
		var elem = checkSet[i];

		if ( elem ) {
			var match = false;
			
			elem = elem[dir];

			while ( elem ) {
				if ( elem.sizcache === doneName ) {
					match = checkSet[elem.sizset];
					break;
				}

				if ( elem.nodeType === 1 ) {
					if ( !isXML ) {
						elem.sizcache = doneName;
						elem.sizset = i;
					}

					if ( typeof cur !== "string" ) {
						if ( elem === cur ) {
							match = true;
							break;
						}

					} else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
						match = elem;
						break;
					}
				}

				elem = elem[dir];
			}

			checkSet[i] = match;
		}
	}
}

if ( document.documentElement.contains ) {
	Sizzle.contains = function( a, b ) {
		return a !== b && (a.contains ? a.contains(b) : true);
	};

} else if ( document.documentElement.compareDocumentPosition ) {
	Sizzle.contains = function( a, b ) {
		return !!(a.compareDocumentPosition(b) & 16);
	};

} else {
	Sizzle.contains = function() {
		return false;
	};
}

Sizzle.isXML = function( elem ) {
	// documentElement is verified for cases where it doesn't yet exist
	// (such as loading iframes in IE - #4833) 
	var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;

	return documentElement ? documentElement.nodeName !== "HTML" : false;
};

var posProcess = function( selector, context ) {
	var match,
		tmpSet = [],
		later = "",
		root = context.nodeType ? [context] : context;

	// Position selectors must be done after the filter
	// And so must :not(positional) so we move all PSEUDOs to the end
	while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
		later += match[0];
		selector = selector.replace( Expr.match.PSEUDO, "" );
	}

	selector = Expr.relative[selector] ? selector + "*" : selector;

	for ( var i = 0, l = root.length; i < l; i++ ) {
		Sizzle( selector, root[i], tmpSet );
	}

	return Sizzle.filter( later, tmpSet );
};

// EXPOSE

window.vglnk.Sizzle = Sizzle;

})();
